我们先看看官方规范针对TCP协议的视音频传输描述:
实时视频点播、历史视频回放与下载的 TCP媒体传输应支持基于RTP封装的视音频PS流,封装格式参照IETFRFC4571。
流媒体服务器宜同时支持作为TCP媒体流传输服务端和客户端。默认情况下,前端设备向流媒体服务器发送媒体流时前端设备应作为TCP媒体流传输客户端,流媒体服务器作为 TCP媒体流传输服务端;同级或跨级流媒体服务器间基于 TCP协议传输视频流时,媒体流的接收方宜作为TCP媒体流传输服务端。
媒体流的发送方和接收方可扩展SDP参数进行TCP媒体流传输服务端和客户端的协商,协商机制参考附录 F及IETFRFC4571的定义。
这里我们看个INVITE信令交互示例:
INVITE sip:34020000001320000001@3402000000 SIP/2.0 Via: SIP/2.0/TCP 192.168.0.105:15060;rport;branch=z9hG4bK630055772 From: <sip:34020000002000000001@3402000000>;tag=562055772 To: <sip:34020000001320000001@3402000000> Call-ID: 589055668 CSeq: 183 INVITE Content-Type: APPLICATION/SDP Contact: <sip:34020000002000000001@192.168.0.105:15060> Max-Forwards: 70 User-Agent: LiveGB28181 Subject: 34020000001320000001:0200000001,34020000002000000001:0 Content-Length: 222 v=0 o=34020000001320000001 0 0 IN IP4 192.168.0.105 s=Play c=IN IP4 192.168.0.105 t=0 0 m=video 30076 RTP/AVP 96 97 98 a=recvonly a=rtpmap:96 PS/90000 a=rtpmap:97 MPEG4/90000 a=rtpmap:98 H264/90000 y=0200000001
判断媒体流走TCP还是UDP,主要看这里:
m=video 30076 RTP/AVP 96 97 98
传输方式采用“RTP/AVP”标识传输层协议为 RTP over UDP,采用“TCP/RTP/AVP”标识传输层协议为 RTP over TCP,需要注意的是,我们实际对接的时候,部分厂商SDP非常随意,有的甚至直接标记个tcp,这让我们对接的时候,很困惑。
技术实现:
本文以大牛直播SDK的Android平台GB28181设备接入端为例,启动GB28181,完成注册、catalog等交互后,Invite上来后,设置媒体流通过TCP还是UDP发送出去:
@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<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; } 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); }
接口设计如下:
/** *设置 RTP Sender传输协议 * * @param rtp_sender_handle, CreateRTPSender返回值 * @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP * * @return {0} if successful */ public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);
以上是GB28181基于TCP协议的视音频媒体传输探究及实现,感兴趣的开发者,可以查看相关协议规范,根据需求实现自己的业务逻辑即可。