RTSP/RTP/RTCP的区别:
RTSP: 客户端和服务器进行信息交流,例如客户端询问服务器支持哪些方法、协议、sps、pps等等,沟通以什么方式建立连接,是否要开始发送数据。
RTP: 服务器以约定好的格式往客户端进行发送封装好的数据;协议提供了时间戳和序列号,发送端在采样时设置时间戳,接收端收到后会按照时间戳依次播放。
RTCP: 当客户端发现RTP丢包的时候,可以通过约定好的格式往服务器发送数据,要求服务器重发数据;UDP是单向通信的,但是这样一来,RTP + RTCP 其实相当于建立起了一个双向对话的机制。
客户端和服务器进行RTSP协议的交互过程:
1、OPTIONS(可选)
//询问服务器端有哪些方法可使用
RTSP客户端 —> RTSP服务器端 OPTIONS命令
//回复客户端服务器支持的方法
RTSP服务器端 —> RTSP客户端 回复OPTIONS命令
2、DESCRIBE (可选)
// 请求对某个媒体资源(Live555中用ServerMediaSession表示)的描述信息
RTSP客户端 —> RTSP服务器端 DESCRIBE命令
//回复客户端某个媒体资源的描述信息(即SDP)
RTSP服务器端 —> RTSP客户端 回复DESCRIBE命令
3、SETUP(必选)
//请求建立对某个媒体资源的连接
RTSP客户端 —> RTSP服务器端 SETUP命令
//回复建立连接的结果
RTSP服务器端 —> RTSP客户端 回复SETUP命令
4、PLAY(必选)
//请求播放媒体资源
RTSP客户端 —> RTSP服务器端 PLAY命令
//回复播放的结果
RTSP服务器端 —> RTSP客户端 回复PLAY命令
5、此外还有一些交互方法,请自行查阅 例如TEARDOWN、PAUSE、SCALE、 GET_PARAMETER、SET_PARAMETER
如果要测试rtsp的解码,可用以下软件:
采集、推流: EasyPusher (商业需要授权)
流媒体服务器 EasyDarwin (完全免费)
测试播放: VLC (完全免费)
相关概念:
网络抖动: 是指最大延迟与最小延迟的时间差,如最大延迟是20毫秒,最小延迟为5毫秒,那么网络抖动就是15毫秒,它主要标识一个网络的稳定性。
MTU: Maximum Transmission Unit 。大部分设备的是1500byte。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。把本机的MTU设成比网关的MTU小或相同,就可以减少丢包。
I帧 : 关键帧,可以理解为这一帧画面的完整保留,解码时只需要本帧数据就可以完成 ,数据量较大。
P帧: 表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)。
B帧: 是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码耗资源。
RTP封包情况:
1、单个NAL包单元:对于 NALU 的长度小于 MTU 大小的包, 一般采用单一 NAL 单元模式;
2、组合封包模式:当 NALU 的长度特别小时, 可以把几个 NALU 单元封在一个 RTP 包中(这种情况用的少);
3、FU-A的分片格式:而当 NALU 的长度超过 MTU 时, 就必须对 NALU 单元进行分片封包,也称为 Fragmentation Units (FUs),现存两个版本FU-A,FU-B。
h264常用的帧类型:
类型 | 说明 |
---|---|
00 00 00 01 06 | 增强帧 |
00 00 00 01 41 | 61和41其实都是P帧(type值为1),只是重要级别不一样 |
00 00 00 01 61 | P帧 |
00 00 00 01 65 | IDR 、I帧 |
00 00 00 01 67 | SPS |
00 00 00 01 68 | PPS |
本人项目中用到的是:单个NAL包单元封装+ FUA分片封装模式
1、客户端每次从服务器接收一个RTP包 ,RTP头 + 数据data;
2、根据data[0] 判断是什么封包模式;
3、如果是单个NAL就直接判断拿到整个NALU;如果是FU_A模式就根据data[1]判断是当前NALU单元是否开始、结束还是中间;如果是开头就需要在开始接收前手动添加当前NALU类型(rtpPackage.data[0]& 0xE0)|(rtpPackage.data[1]&0x1F) -->最后获得F 、NRI 、Type 这三个信息 ,F可以忽略, NRI标记这个NALU的重要性 ,Type标记帧类型);
4、最后获得整个NAUL后 在前面00 00 00 01 传给解码器解码。
关键代码:
RtspClient.java
public class RtspClient
{
private final String TAG="RtspClient" ;
/**是否是tcp方式*/
private boolean isTCPtranslate;
private String hostIp;
private int port;
private String rtspUrl ;
/**rtsp版本号,必须是1.0? 还不确定!*/
private static final String VERSION = " RTSP1.0\r\n";
/**rtsp服务器正确反应标记*/
private static final String RTSP_OK = "RTSP/1.0 200 OK";
/**请求回应对的序列*/
private int seq=1;
/**会话 Session在setup请求后返回 */
private String sessionid=null;
/**trackID 在 请求Descrbe 之后返回*/
private String trackInfo;
/**转换账号和密码*/
private String authorBase64;
/**客户端代理名称随意填*/
private final static String UserAgent = "RtspWalkera/1.0";
private Socket mSocket;
private BufferedReader mBufferreader;
private OutputStream mOutputStream;
/**
* 构造函数 , 进行参数初始化
* @param isTCPtranslate tcp方式还是 udp方式
* @param hostIp 服务器ip
* @param port 服务器端口
* @param rtspUrl rtsp播放地址
* @param authorName 登录账号
* @param authorPassword 登录密码
*/
public RtspClient(boolean isTCPtranslate, String hostIp,int port, String rtspUrl ,String authorName,String authorPassword)
{
this.isTCPtranslate = isTCPtranslate;
this.rtspUrl = rtspUrl;
this.hostIp = hostIp ;
this.port = port;
if(authorName == null && authorPassword == null) {
authorBase64 = null;
} else {
// 返回Base64编码过的字节数组字符串
authorBase64 = Base64.encodeToString((authorName+":"+authorPassword).getBytes(),Base64.DEFAULT);
}
}
private String addHeaders()
{
return "CSeq: " + (seq++) + "\r\n"
+ ((authorBase64 == null)?"":("Authorization: Basic " +authorBase64 +"\r\n"))
+ "User-Agent: " + UserAgent + "\r\n"
+ ((sessionid == null)?"":("Session: " + sessionid + "\r\n"))
+ "\r\n";
}
/**
* option请求 <br>
* 此步骤非必须
*/
public void doOption()
{
StringBuilder sb = new StringBuilder();
sb.append("OPTIONS ");
sb.append(this.rtspUrl.substring(0, rtspUrl.lastIndexOf("/")));
sb.append(VERSION);
sb.append(addHeaders());
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* Describe 请求<br>
* sps 和 pps 会在这个阶段返回
* sprop-parameter-sets= Z0IAKpY1QMgEvTcBAQEC,aM48gA==
* 将 sprop-parameter-sets 对应的值用base64 解码即可 67 和68
* 分别对应sps 和pps . <br>
* 不过有的rtsp服务器如果没有按标准写的话 ,sprop-parameter-sets 返回的值可能为空。
* 此时 如果想获取sps 和pps 只能通过rtsp服务器返回的 帧数据中去获取了。
*/
public void doDescribe()
{
StringBuilder sb = new StringBuilder();
sb.append("DESCRIBE ");
sb.append(this.rtspUrl);
sb.append(VERSION);
sb.append(addHeaders());
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* 请求setup 。 <br>
* RTP/AVP/TCP : RTP采用TCP方式 <br>
* unicast表示单播<br>
* client_port=55640-55641 :前一个是RTP端口 ,后一个是RTCP端口<br>
*
*/
public void doSetup( )
{
StringBuilder sb = new StringBuilder();
if ( isTCPtranslate ) {
sb.append("SETUP ");
sb.append(this.rtspUrl);
sb.append("/");
sb.append(trackInfo);
sb.append(VERSION);
sb.append("Transport: RTP/AVP/TCP;unicast;client_port=55640-55641" + "\r\n");
sb.append(addHeaders());
} else {
sb.append("SETUP ");
sb.append(this.rtspUrl);
sb.append("/");
sb.append(trackInfo);
sb.append(VERSION);
sb.append("Transport: RTP/AVP/UDP;unicast;client_port=55640-55641" + "\r\n");
sb.append(addHeaders());
}
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* play 请求
*/
public void doPlay()
{
StringBuilder sb = new StringBuilder();
sb.append("PLAY ");
sb.append(this.rtspUrl);
sb.append(VERSION);
//设置播放的时间范围
sb.append("Range: npt=0.000-\r\n");
sb.append(addHeaders()) ;
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* Pause 暂停请求
*/
public void doPause() {
StringBuilder sb = new StringBuilder();
sb.append("PAUSE ");
sb.append(this.rtspUrl);
sb.append("/");
sb.append(VERSION);
sb.append(addHeaders()) ;
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* 关闭连接
*/
public void doTeardown()
{
StringBuilder sb = new StringBuilder();
sb.append("TEARDOWN ");
sb.append(this.rtspUrl);
sb.append("/");
sb.append(VERSION);
sb.append(addHeaders()) ;
try
{
outQueue.put(sb.toString());
}catch (Exception e)
{
}
}
/**
* 往服务器发送数据
*/
private void sendBytes(String questStr)
{
try
{
byte[] buffer= questStr.getBytes("UTF-8");
mOutputStream.write(buffer);
mOutputStream.flush();
Log.i(TAG ,"请求服务器:"+questStr +" "+outQueue.size());
}catch (Exception e)
{
Log.i(TAG ,"请求服务器异常:"+e.getMessage());
}
}
private boolean isStopReceive =false ;
/***
* rtsp 接收子线程
*/
private class ReceiveThread extends Thread
{
@Override
public void run()
{
while(!isStopReceive)
{
try
{
if(mBufferreader!=null)
{
String line ;
if( (line = mBufferreader.readLine()) != null)
{
Log.i(TAG ,"收到服务器返回=" + line);
}else {
Log.i(TAG ,"收到服务器返回 line==null" );
}
}
//休眠一段时间
Thread.sleep(60);
}catch (Exception e)
{
Log.i(TAG ,"接收服务器异常" +e.getLocalizedMessage());
}
}
}
}
private LinkedBlockingDeque<byte[]> rtpDataBuffer = new LinkedBlockingDeque<>();
private DatagramSocket mUdpSocket;
private DatagramPacket mUdpPackets;
int udpPort = 55640; //55640- 55641
private byte[] message = new byte[2048];
/***
* upd rtp 接收子线程
*/
private class ReceiveRTPUdpThread extends Thread
{
@Override
public void run()
{
while(!isStopReceive)
{
try
{
if(mUdpSocket==null)
{
mUdpSocket = new DatagramSocket(udpPort);
mUdpPackets = new DatagramPacket(message,message.length);
}else{
mUdpSocket.receive(mUdpPackets);
byte[] buffer = new byte[mUdpPackets.getLength()];
System.arraycopy(mUdpPackets.getData(), 0, buffer, 0, mUdpPackets.getLength());
try {
rtpDataBuffer.put(buffer);
} catch (InterruptedException e) {
Log.e(TAG,"The buffer queue is full , wait for the place..");
}
}
//休眠一段时间
Thread.sleep(0);
}catch (Exception e)
{
Log.i(TAG ,"udp接收服务器异常" +e.getLocalizedMessage());
}
}
}
}
public byte[]getRtpData()
{
byte[] nalu =null;
if(rtpDataBuffer !=null && rtpDataBuffer.size()>0)
{
try {
nalu = rtpDataBuffer.take();
}catch (Exception e)
{
}
}
return nalu ;
}
/**发送队列*/
private BlockingQueue<String> outQueue = new LinkedBlockingQueue< >(Integer.MAX_VALUE);;
/**
* 发送子线程
*/
private class SendThread extends Thread
{
@Override
public void run()
{
while(!isStopReceive)
{
try
{
String buffer = outQueue.poll();
if (buffer != null)
{
sendBytes(buffer);
}
//休眠一段时间
Thread.sleep(60);
}catch (Exception e)
{
}
}
}
}
/**
* 开启rtsp客户端
*/
public void openRtspCline()
{
isStopReceive = false ;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try{
mSocket = new Socket(hostIp, port);
mBufferreader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
mOutputStream = mSocket.getOutputStream();
}catch (IOException e)
{
Log.i(TAG,"创建socket出现异常="+e.getLocalizedMessage()) ;
}
}
});
thread.start();
ReceiveThread receiveThread = new ReceiveThread();
receiveThread.start();
SendThread sendThread = new SendThread() ;
sendThread.start();
ReceiveRTPUdpThread receiveRTPUdpThread = new ReceiveRTPUdpThread();
receiveRTPUdpThread.start();
}
/**
* 关闭rtsp客户端
*/
public void shutDownRtspClient()
{
isStopReceive = true ;
}
}
RtpPackage.java
/**
* RTP 数据包 结构
*/
public class RtpPackage
{
/**版本是 2*/
public int V ;
/**填充标志*/
public int P ;
/**拓展标志*/
public int X ;
/**csrc计数器*/
public int CC ;
/**音频、视频标记*/
public int M ;
/**多媒体的类型 H.264的类型为96*/
public int PT;
/**序列号*/
public int sequenceNumber;
/**时间戳*/
public long timestamp;
/**ssrc*/
public long ssrcIdentifier;
/**csrc列表*/
public int csrcIdentiferList ;
/**rtp 包数据*/
public byte[] data;
}
RTP2NaluTools.java
public class RTP2NaluTools
{
private final String TAG="RTP2NaluTools" ;
private final static int NAL_UNIT_TYPE_STAP_A = 24;
private final static int NAL_UNIT_TYPE_STAP_B = 25;
private final static int NAL_UNIT_TYPE_MTAP16 = 26;
private final static int NAL_UNIT_TYPE_MTAP24 = 27;
private final static int NAL_UNIT_TYPE_FU_A = 28;
private final static int NAL_UNIT_TYPE_FU_B = 29;
/**一帧数据(一个NALU)*/
private byte[] NALUnit;
/**NALU结束标记*/
private boolean NALEndFlag;
/**临时变量 buffer[i] 存储每一片数据 */
private byte[][] buffer = new byte[1024][];
/**当前已经接收的NALU的数据长度*/
private int bufferLength;
/**当前已经接收的数据片数量*/
private int packetNum;
public RTP2NaluTools()
{
NALEndFlag = false;
bufferLength= 0;
packetNum =0;
}
/**
* 将 rtp包 组成 NALU 格式的数据 <br>
* 返回一帧H264数据(一个NALU)
*/
public byte[] rtpPackage2NALU(byte[] rtpData)
{
if(rtpData.length == 0)
{
return null ;
}
RtpPackage rtpPackage = new RtpPackage();
rtpPackage.V=(rtpData[0]&0xFF)>>6 ;
rtpPackage.PT = rtpData[1] & 0x7F;
rtpPackage.data = new byte[rtpData.length-12];
System.arraycopy(rtpData, 12, rtpPackage.data, 0, rtpData.length - 12);
if( (rtpPackage.V != 2)&&(rtpPackage.PT!=96) || rtpPackage.data.length <2 )
{
return null;
}
// rtp封包方式,单一封包、组合封包、分片封包等 0x1F -> 0001 1111
int fuIndicatorTypeFlag = rtpPackage.data[0] & 0x1F;
// FU head中的 S、E标记 用来判断NALU的开始、结束 0xCO -> 1100 0000 取S、E
int fuHeadSEFlag = rtpPackage.data[1] & 0xC0;
switch (fuIndicatorTypeFlag)
{
case NAL_UNIT_TYPE_STAP_A:
break;
case NAL_UNIT_TYPE_STAP_B:
break;
case NAL_UNIT_TYPE_MTAP16:
break;
case NAL_UNIT_TYPE_MTAP24:
break;
case NAL_UNIT_TYPE_FU_B:
break;
case NAL_UNIT_TYPE_FU_A:
switch (fuHeadSEFlag)
{
//NAL Unit start packet 0x80-> 1000 0000
case 0x80:
NALEndFlag = false;
bufferLength = rtpPackage.data.length-1 ;
buffer[0] = new byte[rtpPackage.data.length-1];
/*
每一帧开始的时候要加上表示这个帧类型的信息。
0xE0 ->1110 0000 0x1F -> 0001 1111
取FU indicator的前三位和FU Header的后五位。
最后这个byte包含了 F 、NRI 、Type 这三个信息
F可以忽略, NRI标记这个NALU的重要性 ,Type标记帧类型
buffer[0][0] 帧类型数据 41、61、65、67、68等
*/
buffer[0][0] = (byte)((rtpPackage.data[0] & 0xE0)|(rtpPackage.data[1]&0x1F));
System.arraycopy(rtpPackage.data,2,buffer[0],1,rtpPackage.data.length-2);
packetNum = 1;
break;
//NAL Unit middle packet 0x00->0000 0000
case 0x00:
NALEndFlag = false;
bufferLength += rtpPackage.data.length-2;
buffer[packetNum] = new byte[rtpPackage.data.length-2];
System.arraycopy(rtpPackage.data,2,buffer[packetNum],0,rtpPackage.data.length-2);
packetNum++;
break;
//NAL Unit end packet 0x40 -->0100 0000
case 0x40:
NALEndFlag = true;
//+4 是因为多了 00 00 00 01
NALUnit = new byte[bufferLength + rtpPackage.data.length- 2 + 4];
//因为打包发送的时候是去掉了00 01这种标记位的,为了处理后续的H264,需要手动加上
NALUnit[0] = 0x00;
NALUnit[1] = 0x00;
NALUnit[2] = 0x00;
NALUnit[3] = 0x01;
int tmpLen = 4;
//第一片 + 中间片 数据
for(int i = 0; i < packetNum; ++i)
{
System.arraycopy(buffer[i],0,NALUnit,tmpLen,buffer[i].length);
tmpLen += buffer[i].length;
}
//最后一片的数据
System.arraycopy(rtpPackage.data,2,NALUnit,tmpLen,rtpPackage.data.length-2);
break;
}
break;
//这里采用排除法,排除了组包封装和分片封装 之后就认为是 单个打包模式
default:
NALUnit = new byte[4+rtpPackage.data.length];
NALUnit[0] = 0x00;
NALUnit[1] = 0x00;
NALUnit[2] = 0x00;
NALUnit[3] = 0x01;
System.arraycopy(rtpPackage.data,0,NALUnit,4,rtpPackage.data.length);
NALEndFlag = true;
break;
}
if(NALEndFlag)
{
return NALUnit;
}else{
return null;
}
}
public void stop()
{
NALUnit = null;
}
}
使用方法:
boolean isTCPtranslate = false ; //false true
String hostIp ="192.168.1.235";
int port= 554 ;
String rtspUrl ="rtsp://192.168.1.235:554/";
String authorName=null ;
String authorPassword=null ;
RtspClient rtspClient = new RtspClient( isTCPtranslate, hostIp, port, rtspUrl , authorName, authorPassword);
rtspClient.openRtspCline();
//请求options
rtspClient.doOption();
//请求describe
rtspClient.doDescribe();
//请求建立连接(此阶段指明用Tcp或UDP建立连接)
rtspClient.doSetup();
//请求开始下发Rtp包
rtspClient.doPlay();
//开启子线程接收RTP数据
byte[] rtpData =rtspClient.getRtpData() ;
//对Rtp数据进行解析,获得H264数据
byte[] nalu= rtp2NaluTools.rtpPackage2NALU(rtpData) ;