GB28181-2022针对“基于TCP协议的视音频媒体传输”实时点播、历史视频回放与下载中,TCP媒体传输重连机制,做了说明。
修改后的“基于TCP协议的视音频媒体传输要求”如下:
实时视频点播、历史视频回放与下载的TCP媒体传输应支持基于RTP封装的视音频PS流,封装格式参照IETF RFC 4571。
流媒体服务器宜同时支持作为TCP媒体流传输服务端和客户端。在默认情况下,前端设备向流媒体服务器发送媒体流时,前端设备应作为TCP媒体流传输客户端,流媒体服务器作为TCP媒体流传输服务端;同级或跨级流媒体服务器间基于TCP协议传输视频流时,媒体流的接收方宜作为TCP媒体流传输服务端。
媒体流的发送方和接收方可扩展SDP参数进行TCP媒体流传输服务端和客户端的协商,协商机制应符合附录G及IETF RFC 4571的定义。
实时视频点播、历史视频回放与下载的TCP媒体传输在建立TCP连接时应支持重连机制。首次TCP连接失败,TCP媒体流传输客户端应间隔一段时间进行重连,重连间隔应不小于l s,重连次数应不小于3次。
本文以大牛直播SDK实现的Andorid平台GB28181设备接入模块为例,收到Invite处理如下,其中SetRTPSenderTransportProtocol()设置TCP/UDP传输模式:
ntsOnInvitePlay()处理代码如下:
// Author: daniusdk.com
@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;
// 28181 视频使用PS打包
Vector 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;
}
if (null == ps_rtpmap_attr) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay get ps rtp map attribute 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_ = ps_rtpmap_attr.getPayloadType();
gb28181_rtp_encoding_name_ = ps_rtpmap_attr.getEncodingName();
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, 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(ps_rtpmap_attr.getPayloadType()));
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后:
// Author: daniusdk.com
@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;
}
if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) {
CheckInitAudioRecorder();
}
startLayerPostThread();
isGB28181StreamRunning = true;
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
TCP媒体传输重连机制,非常必要,实际上在2022出来之前,我们也已经做了很好的重连处理,GB28181-2022对此专门做了详细的解释说明,具体实现难度不大,感兴趣的开发者可以酌情参考。