技術的背景
GB/T28181-2016 の公式仕様と対話プロセスを繰り返すつもりはありません。
SIP サーバーによって開始されるブロードキャスト プロセスの概略図は次のとおりです。
注: 音声ブロードキャスト通知、音声ブロードキャスト応答コマンド
メッセージ ヘッダーの Content-type フィールドは、Content-type:Application/MANSCDP+xml です。
音声ブロードキャスト通知コマンドと音声ブロードキャスト応答コマンドは、MANSCDP プロトコル形式で定義されています。
メッセージの例は次のとおりです。
a) 音声ブロードキャスト通知
MESSAGE sip:[email protected]:6720 SIP/2.0\
Via: SIP/2.0/UDP 192.168.100.10:5060;rport=5060;branch=z9hG4bK1073741856;received=192.168.100.10\
From: <sip:[email protected]:5060>;tag=912513446\
To: <sip:[email protected]:6720>\
Call-ID: 536870958\
CSeq: 1 MESSAGE\
Contact: <sip:[email protected]:5060>\
Content-Type: Application/MANSCDP+xml\
Max-Forwards: 70\
User-Agent: Hikvision\
Content-Length: 172\
\
<?xml version="1.0"?>\
<Notify>\
<CmdType>Broadcast</CmdType>\
<SN>11</SN>\
<SourceID>34020000002000000001</SourceID>\
<TargetID>34020000001310000056</TargetID>\
</Notify>\
b) 音声ブロードキャスト応答
MESSAGE sip:[email protected]:5060 SIP/2.0\
Call-ID: [email protected]\
CSeq: 18300138 MESSAGE\
From: <sip:34020000001310000056@3402000000>;tag=4a5b3953\
To: <sip:[email protected]:5060>\
Via: SIP/2.0/UDP 192.168.100.9:6720;rport;branch=z9hG4bK-373435-549b6376963815eb98e2a2f011473b41\
Max-Forwards: 70\
User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\
Content-Type: Application/MANSCDP+xml\
Content-Length: 173\
\
<?xml version="1.0" encoding="GB2312"?>\
<Response>\
<CmdType>Broadcast</CmdType>\
<SN>11</SN>\
<DeviceID>34020000001310000056</DeviceID>\
<Result>OK</Result>\
</Response>\
c) プラットフォーム側は 200 OK で応答します。
SIP/2.0 200 OK\
Via: SIP/2.0/UDP 192.168.100.9:6720;rport;branch=z9hG4bK-373435-549b6376963815eb98e2a2f011473b41\
From: <sip:34020000001310000056@3402000000>;tag=4a5b3953\
To: <sip:[email protected]:5060>\
Call-ID: [email protected]\
CSeq: 18300138 MESSAGE\
User-Agent: Hikvision\
Content-Length: 0\
d) デバイスのアクセス側が招待リクエストを開始します。
INVITE sip:34020000002000000001@3402000000 SIP/2.0\
...
User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\
Content-Type: APPLICATION/SDP\
Content-Length: 245\
\
v=0\
o=34020000001310000056 3898650599696 3898650599696 IN IP4 192.168.100.9\
s=Play\
c=IN IP4 192.168.100.9\
t=0 0\
m=audio 25000 TCP/RTP/AVP 8\
a=setup:active\
a=connection:new\
a=recvonly\
a=rtpmap:8 PCMA/8000\
y=0200009722\
f=v/a/1/8/1\
e) 国家標準プラットフォーム側は 200 OK で応答します。
SIP/2.0 200 OK\
...
Content-Type: application/sdp\
User-Agent: Hikvision\
Content-Length: 205\
\
v=0\
o=34020000002000000001 0 0 IN IP4 192.168.100.10\
s=Play\
c=IN IP4 192.168.100.10\
t=0 0\
m=audio 16002 TCP/RTP/AVP 8\
a=rtpmap:8 PCMA/8000\
a=sendonly\
a=setup:passive\
y=0200009727\
f=v/a/1/8/1\
f) デバイスのアクセス側が Ack を送信します。
ACK sip:[email protected]:5060 SIP/2.0\
...
Max-Forwards: 70\
Contact: <sip:[email protected]:6720>\
User-Agent: NT GB UserAgent V1.91-20230420[daniusdk.com]\
Content-Length: 0\
技術的な実現
Daniu Live SDK の Android プラットフォーム GB28181 デバイス アクセス側を例に挙げます。
音声ブロードキャストを受信する:
@Override
public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "daniusdk, ntsOnNotifyBroadcastCommand, fromUserName:"+ from_user_name_ + ", fromUserNameAtDomain:"+ from_user_name_at_domain_
+ ", SN:" + sn_ + ", sourceID:" + source_id_ + ", targetID:" + target_id_);
if (gb28181_agent_ != null ) {
gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true);
btnGB28181AudioBroadcast.setText("收到GB28181语音广播通知");
}
}
private String from_user_name_;
private String from_user_name_at_domain_;
private String sn_;
private String source_id_;
private String target_id_;
public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) {
this.from_user_name_ = from_user_name;
this.from_user_name_at_domain_ = from_user_name_at_domain;
this.sn_ = sn;
this.source_id_ = source_id;
this.target_id_ = target_id;
return this;
}
}.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0);
}
ntsOnAudioBroadcast 処理:
@Override
public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnAudioBroadcastPlay, fromFromUserName:" + command_from_user_name_
+ " FromUserNameAtDomain:" + command_from_user_name_at_domain_
+ " sourceID:" + source_id_ + ", targetID:" + target_id_);
stopAudioPlayer();
destoryRTPReceiver();
if (gb28181_agent_ != null ) {
String local_ip_addr = IPAddrUtils.getIpAddress(context_);
boolean is_tcp = true; // 考虑到跨网段, 默认用TCP传输rtp包
rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0);
if (rtp_receiver_handle_ != 0 ) {
lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0);
lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0);
if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) {
int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_);
boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_,
source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP");
if (!ret ) {
destoryRTPReceiver();
btnGB28181AudioBroadcast.setText("GB28181语音广播");
}
else {
btnGB28181AudioBroadcast.setText("GB28181语音广播呼叫中");
}
} else {
destoryRTPReceiver();
btnGB28181AudioBroadcast.setText("GB28181语音广播");
}
}
}
}
private String command_from_user_name_;
private String command_from_user_name_at_domain_;
private String source_id_;
private String target_id_;
public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) {
this.command_from_user_name_ = command_from_user_name;
this.command_from_user_name_at_domain_ = command_from_user_name_at_domain;
this.source_id_ = source_id;
this.target_id_ = target_id;
return this;
}
}.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0);
}
ブロードキャスト応答の処理:
@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;
List<SDPRtpMapAttribute> audio_attrs = new LinkedList<>();
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_attrs.clear();
Vector<SDPRtpMapAttribute> rtp_maps = m.getRtpMapAttributes();
for (SDPRtpMapAttribute a : rtp_maps) {
int type = a.getPayloadType();
String name = a.getEncodingName();
if (0 == type || 8 == type)
audio_attrs.add(a);
else if (name != null && !name.isEmpty()) {
if (name.equals("PS") || name.equals("PCMA") || name.equals("PCMU"))
audio_attrs.add(a);
}
}
if (!audio_attrs.isEmpty()) {
audio_des = m;
break;
}
}
}
}
if (audio_des != null && !audio_attrs.isEmpty() ) {
// 有些场景下 SDP.SSRC 和 RTP.SSRC 不相等, 对于这种情况,不要设置SSRC给SDK, 屏蔽掉下面这行设置SSRC的代码
lib_player_.SetRTPReceiverSSRC(rtp_receiver_handle_, audio_des.getSSRC());
....
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);
}
なお、上記の GB28181 プラットフォーム メーカーを例に挙げると、SDP は PCMA をネゴシエートしますが、実際には PS のオーディオ データはプラットフォーム側で配信されます。PS が設定されていない場合、次のログが記録されます。現れる:
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo I/NTLogAndroid: NTRTP readSource: received rtp packet, is_udp:0, payload_type=96, len=232, t:0, sn=0, ssrc=200009727, src_address:0.0.0.0:0\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.193 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.250 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.277 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
2023-07-18 14:30:05.277 9248-9574/com.smartpublisher.camera2demo E/NTLogAndroid: NTRTP readSource could not find configuration for payload type:96\
したがって、一般的なアプローチは、SDP に PS があるかどうかを判断し、ない場合は RTP 受信機のペイロード タイプを設定することです。
要約する
なぜどの企業も Android プラットフォーム上の GB28181 デバイスのアクセス側でこれを実行しようとしないのでしょうか? 本当に落とし穴が多すぎて、GB28181 メーカーが多すぎて、大手メーカーを含む多くのメーカーが仕様に厳密に従っていません。簡単に言えば、エネルギーの 50% はコードの記述であり、エネルギーの 50% はチェックされます。問題や各種互換処理など。