接口描述
国网B接口调阅实时视频,相关规范写的比较粗略:
调阅实时视频包括信令接口和媒体流接口,采用标准的SIP INVITE SDP流程,媒体传输使用RTP/RTCP。
SDP 中 RTP Payload 的取值应遵守下面接口参数中的定义:
a) SDP 中的媒体信息,应仅有一个 m 行,用于描述视频格式。
b) 视频数据用 RTP 打包传输时,应考虑每个传输分组不大于 MTU,可采用的技术包括编码器层支持(如 ITU-T H.264 的 multi-slice 技术),或采用 RTP 层的分片机制(如 IETF RFC 3984 定义的 FU-A 技术)。
前端设备收到平台的INVITE请求后根据SDP描述进行媒体协商,协商通过后打开前端系统摄像机设备将获得的媒体流通过媒体通道发送到平台。
会话建立成功后,前端系统在某些特殊情况下可以主动结束当前呼叫。
平台应支持视频流的分发,以降低对前端系统的操作频繁性和节省网络带宽。
调阅实时视频的接口流程
主要功能流程如下:
a) F1:用户发送 INVITE 消息,携带 SDP 内容通过平台转发到前端设备。
b) F2:按照 SIP 要求,如前端系统在 0.5s 内未能处理该请求,则先发送 1xx 临时响应通过平台转发到用户。
c) F3:前端系统接受了调阅请求的操作,则发送携带 SDP 的 200 OK 响应通过平台转发到用户。
d) F4:用户发送 ACK 通过平台转发到前端设备。
e) 视频流从前端系统传输经平台转发到用户。
f) F5:用户结束会话,发送 BYE 消息到通过平台转发到前端系统。
g)F6:前端系统发送确认,将媒体通道拆线。
技术实现
由于国网B接口的invite实现和GB28181的差异不大,之前我们GB28181这块,已经有非常好的积累了。
编辑 启动B接口后,完成平台端的register和PushResourse交互,有些平台注册后,会接着响应Push_Resource request,其他不表,这里主要谈下invte和Ack相关回调处理:
Invite信令如下:
代码语言:javascript复制 INVITE sip:1301110005010100001@192.168.0.102:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.104:15060;branch=z9hG4bK864531896
From: <sip:000000000000000001@0000000000>;tag=482531896
To: <sip:1301110005010100001@192.168.0.102:5060>
Call-ID: 804531783
CSeq: 6 INVITE
Content-Type: application/sdp
Contact: <sip:000000000000000001@192.168.0.104:15060>
Max-Forwards: 70
User-Agent: SIPB
Request-URI: <sip:1301110005010100001@192.168.0.102:5060>
Content-Length: 152
v=0
o=- 0 0 IN IP4 192.168.0.104
s=Play
c=IN IP4 192.168.0.104
t=0 0
m=video 30004 RTP/AVP 100
a=recvonly
a=rtpmap:100 H264/90000
y=0130111000
收到Invite回调处理逻辑如下:
代码语言:javascript复制 @Override
public void ntsOnInvitePlay(String deviceId, SessionDescription session_des) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
// 先振铃响应下
gb28181_agent_.respondPlayInvite(180, device_id_);
MediaSessionDescription video_des = null;
SDPRtpMapAttribute ps_rtpmap_attr = null;
Vector<MediaSessionDescription> video_des_list = session_des_.getVideoPSDescriptions();
if (video_des_list != null && !video_des_list.isEmpty()) {
for(MediaSessionDescription m : video_des_list) {
if (m != null && m.isValidAddressType() && m.isHasAddress() ) {
video_des = m;
ps_rtpmap_attr = video_des.getPSRtpMapAttribute();
break;
}
}
}
if (null == video_des) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay get video description is null, response 488, device_id:" device_id_);
return;
}
Log.i(TAG,"ntsOnInvitePlay, device_id:" device_id_ ", is_tcp:" video_des.isRTPOverTCP()
" rtp_port:" video_des.getPort() " ssrc:" video_des.getSSRC()
" address_type:" video_des.getAddressType() " address:" video_des.getAddress());
long rtp_sender_handle = libPublisher.CreateRTPSender(0);
if ( rtp_sender_handle == 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay CreateRTPSender failed, response 488, device_id:" device_id_);
return;
}
gb28181_rtp_payload_type_ = 100;
gb28181_rtp_encoding_name_ = "PS";
libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1);
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1);
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
libPublisher.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2M
libPublisher.SetRTPSenderClockRate(rtp_sender_handle, 90000 /*ps_rtpmap_attr.getClockRate()*/);
libPublisher.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort());
if ( libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle);
if (local_port == 0) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
Log.i(TAG,"get local_port:" local_port);
String local_ip_addr = IPAddrUtils.getIpAddress(context_);
MediaSessionDescription local_video_des = new MediaSessionDescription(video_des.getType());
local_video_des.addFormat(String.valueOf(100));
local_video_des.addRtpMapAttribute(ps_rtpmap_attr);
local_video_des.setAddressType(video_des.getAddressType());
local_video_des.setAddress(local_ip_addr);
local_video_des.setPort(local_port);
local_video_des.setTransportProtocol(video_des.getTransportProtocol());
local_video_des.setSSRC(video_des.getSSRC());
if (!gb28181_agent_.respondPlayInviteOK(device_id_,local_video_des) ) {
libPublisher.DestoryRTPSender(rtp_sender_handle);
Log.e(TAG, "ntsOnInvitePlay call respondPlayInviteOK failed.");
return;
}
gb28181_rtp_sender_handle_ = rtp_sender_handle;
}
private String device_id_;
private SessionDescription session_des_;
public Runnable set(String device_id, SessionDescription session_des) {
this.device_id_ = device_id;
this.session_des_ = session_des;
return this;
}
}.set(deviceId, session_des),0);
}
ack信令如下:
代码语言:javascript复制 ACK sip:1301110005010100001@192.168.0.102:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.104:15060;branch=z9hG4bK991532349
From: <sip:000000000000000001@0000000000>;tag=482531896
To: <sip:1301110005010100001@192.168.0.102:5060>;tag=2d6ebb3d
Call-ID: 804531783
CSeq: 6 ACK
Contact: <sip:000000000000000001@192.168.0.104:15060>
Max-Forwards: 70
User-Agent: SIPB
Request-URI: <sip:1301110005010100001@192.168.0.102:5060>
Content-Length: 0
ack回调代码处理逻辑如下:
代码语言:javascript复制 @Override
public void ntsOnAckPlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsOnACKPlay, device_id:" device_id_);
if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) {
InitAndSetConfig();
}
libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);
int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
if (startRet != 0) {
if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) {
if (publisherHandle != 0) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
}
destoryRTPSender();
Log.e(TAG, "Failed to start GB28181 service..");
return;
}
startAudioRecorder();
startLayerPostThread();
isGB28181StreamRunning = true;
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
总结
国网B接口调阅实时视频流程和GB28181流程基本一致,感兴趣的开发者,可以参考相关的规范实现,B接口相对GB28181来说,面更窄,资料也更少,如果产品化,有测试平台的话,还是不难实现的。