本文为作者原创文章,未经同意禁止转载
RTP协议被定义为一个用于在IP网络上实时传输音视频数据的标准数据包格式,它被广泛应用于通信和娱乐系统中,包括流媒体、视频会议、电视服务等等。
RTP可以提供低延时的数据传送服务,但无法保证数据包到达客户端时仍然保持着发送时的顺序,所以要依靠RTCP来完成流量控制和拥塞监控。
RTP协议时运行在UDP协议之上的,在OSI七层模型种,它们运行在传输层。其他底层协议也可以和RTP协同工作。RTP本身未包含任何服务质量保证机制,因此RTP无法保持接收到的数据域传送顺序相同,也不去验证底层网络所提供的服务是否可靠。RTP通过在数据包中添加序列号来使得接收方能够在收到数据包后通过检测序列号对数据包重新排序,以达到有序的目的。
在流媒体数据传输这个问题上,人们通常面临这同一个严峻的问题,那就是数据传送到客户端的时间点时无法预测的,但是,流媒体的传送要保证数据能够在合适的时间传送目的地以完成数据正确的回放。
因此,RTP协议中为了控制实时数据流传输而包含时间戳、序列号等结构。时间戳在流媒体传送的过程中扮演了重要的校色,为客户端提供了重要的数据。在发送RTP报文时,发送端在数据包里放置了用于记录采样时间的数据,称为时间戳,数据包经IP网络到达接收端后,接收端则需要从数据包中提取出该数据端,依照它来恢复原始的数据次序。RTP只是传输层协议,并不负责同步。RTP将这部分功能留给应用层来完成,已达到简化数据处理、提高运行效率的目的。
RTP的数据单元是用UDP分组来承载的,当然并不是简单地将一个RTP数据单元封装在UDP分组中,而是把一帧数据分割后放入多个UDP分组中传送,本属于同一帧数据的UDP分组将具有完全一样的时间戳。
在RTP会话期间,RTCP协议的作用主要是传送监控数据传送正确率的交换控制信息。在会话期间,与会者们将会发送一些相关的统计信息给其他用户,例如已发送的数据包的数量和无法传送的数据包的数量,这些信息的发送通常每隔相同的时间进行一次,在会话进行过程中周期性完成。
服务器将解析这些数据,并据此提高或降低数据的发送速率,也有可能换用其它类型的有效载荷。这些改变将是动态进行的。RTP配合RTCP运行,通过返回控制信息和减小带宽消耗提升传输效率,从而保证数据传输的时延最小。
RTCP主要有4个功能:通过反馈分配数据的传送质量来进行拥塞控制、监视网络和诊断网络中的问题;由于SSRC(同步源标识)并不会一成不变,当网络拥塞发生时或者与会者程序发生变化时SSRC多会随之更新,我们需要为RTP源提供额外的传送层标志;调整RTCP包的发送速度,以保证数据包能顺利到达接收端,因此需要依据参与者的数据来进行调整;传送会话控制信息。
RTP的报文结构
RTP报文由两部分组成:报头和有效载荷,共12个字节。报头将包含有一些与报文内容相关的参数,例如当前报文的序列号用于在接收端恢复数据和检测数据丢失、报文采样时的时间戳、以及同步源标识符(SSRC)用来区分多个信源和报文负载中所包含数据的负载类型。报头格式如下图:
版本号(V):2比特,用来标志使用的RTP版本。
填充位(P):1比特,如果该位置位,则该RTP包的尾部就包含附加的填充字节。
扩展位(X): 1比特,如果该位置位的话,RTP固定头部后面就跟有一个扩展头部。
CSRC计数器(CC):4比特,含有固定头部后面跟着的CSRC的数目。
标记位(M): 1比特,该位的解释由配置文档(Profile)来承担.
载荷类型(PayloadType): 7比特,标识了RTP载荷的类型。
序列号(SN):16比特,每发送一个 RTP 数据包,序列号增加1。接收端可以据此检测丢包和重建包序列。
时间戳(Timestamp): 2比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。即使在没有信号发送时,时间戳的数值也要随时间而不断地增加。时钟频率依赖于负载数据格式,并在描述文件(profile)中进行描述。
同步源标识符(SSRC):32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法。
贡献源列表(CSRC):0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份。
RTP协议的工作流程 我们通常使用5060和5061两个端口,分别分配给RTP和RTCP。在RTP会话的过程中,RTCP包将会以周期性的形式被每个与会者发送。
RTP的工作机制为:在创建RTP会话前,需要确定一个网络地址和一对端口号构成的目标传送地址。
这两个端口将分别用于收发RTP数据包和RTCP数据包。通常使用偶数端口作为RTP包的目的端口,而对应+1的奇数号端口用做RTCP包的目标端口。
上层应用将视频数据流传递给下层的RTP协议,加上RTP报头后被封装成RTP数据包,同时上层也同时向下传送控制信息给RTCP协议,这些控制信息将被打包成RTCP控制包,而后RTP协议将会把RTP数据包发往目的传送地址的偶数号端口,而RTCP协议则将RTCP包传送到目标传送地值的奇数号端口。
RTP协议的Java实现
RtpSocket时RTP协议的套接字。
/**
* A basic implementation of an RTP socket.
* It implements a buffering mechanism, relying on a FIFO of buffers and a Thread.
* That way, if a packetizer tries to send many packets too quickly, the FIFO will
* grow and packets will be sent one by one smoothly.
*/
public class RtpSocket implements Runnable {
public static final String TAG = "RtpSocket";
public static final int RTP_HEADER_LENGTH = 12;
public static final int MTU = 1300;
private MulticastSocket mSocket;
private DatagramPacket[] mPackets;
private byte[][] mBuffers;
private long[] mTimestamps;
private SenderReport mReport;
private Semaphore mBufferRequested, mBufferCommitted;
private Thread mThread;
private long mCacheSize;
private long mClock = 0;
private long mOldTimestamp = 0;
private int mSsrc, mSeq = 0, mPort = -1;
private int mBufferCount, mBufferIn, mBufferOut;
private int mCount = 0;
private AverageBitrate mAverageBitrate;
/**
* This RTP socket implements a buffering mechanism relying on a FIFO of buffers and a Thread.
* @throws IOException
*/
public RtpSocket() {
mCacheSize = 00;
mBufferCount = 300; // TODO: reajust that when the FIFO is full
mBuffers = new byte[mBufferCount][];
mPackets = new DatagramPacket[mBufferCount];
mReport = new SenderReport();
mAverageBitrate = new AverageBitrate();
resetFifo();
for (int i=0; i<mBufferCount; i++) {
mBuffers[i] = new byte[MTU];
mPackets[i] = new DatagramPacket(mBuffers[i], 1);
/* Version(2) Padding(0) */
/* ^ ^ Extension(0) */
/* | | ^ */
/* | -------- | */
/* | |--------------------- */
/* | || -----------------------> Source Identifier(0) */
/* | || | */
mBuffers[i][0] = (byte) Integer.parseInt("10000000",2);
/* Payload Type */
mBuffers[i][1] = (byte) 96;
/* Byte 2,3 -> Sequence Number */
/* Byte 4,5,6,7 -> Timestamp */
/* Byte 8,9,10,11 -> Sync Source Identifier */
}
try {
mSocket = new MulticastSocket();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
private void resetFifo() {
mCount = 0;
mBufferIn = 0;
mBufferOut = 0;
mTimestamps = new long[mBufferCount];
mBufferRequested = new Semaphore(mBufferCount);
mBufferCommitted = new Semaphore(0);
mReport.reset();
mAverageBitrate.reset();
}
/** Closes the underlying socket. */
public void close() {
mSocket.close();
}
/** Sets the SSRC of the stream. */
public void setSSRC(int ssrc) {
this.mSsrc = ssrc;
for (int i=0;i<mBufferCount;i++) {
setLong(mBuffers[i], ssrc,8,12);
}
mReport.setSSRC(mSsrc);
}
/** Returns the SSRC of the stream. */
public int getSSRC() {
return mSsrc;
}
/** Sets the clock frquency of the stream in Hz. */
public void setClockFrequency(long clock) {
mClock = clock;
}
/** Sets the size of the FIFO in ms. */
public void setCacheSize(long cacheSize) {
mCacheSize = cacheSize;
}
/** Sets the Time To Live of the UDP packets. */
public void setTimeToLive(int ttl) throws IOException {
mSocket.setTimeToLive(ttl);
}
/** Sets the destination address and to which the packets will be sent. */
public void setDestination(InetAddress dest, int dport, int rtcpPort) {
mPort = dport;
for (int i=0;i<mBufferCount;i++) {
mPackets[i].setPort(dport);
mPackets[i].setAddress(dest);
}
mReport.setDestination(dest, rtcpPort);
}
public int getPort() {
return mPort;
}
public int getLocalPort() {
return mSocket.getLocalPort();
}
public SenderReport getRtcpSocket() {
return mReport;
}
/**
* Returns an available buffer from the FIFO, it can then be modified.
* Call {@link #commitBuffer(int)} to send it over the network.
* @throws InterruptedException
**/
public byte[] requestBuffer() throws InterruptedException {
mBufferRequested.acquire();
mBuffers[mBufferIn][1] &= 0x7F;
return mBuffers[mBufferIn];
}
/** Puts the buffer back into the FIFO without sending the packet. */
public void commitBuffer() throws IOException {
if (mThread == null) {
mThread = new Thread(this);
mThread.start();
}
if (++mBufferIn>=mBufferCount) mBufferIn = 0;
mBufferCommitted.release();
}
/** Sends the RTP packet over the network. */
public void commitBuffer(int length) throws IOException {
updateSequence();
mPackets[mBufferIn].setLength(length);
mAverageBitrate.push(length);
if (++mBufferIn>=mBufferCount) mBufferIn = 0;
mBufferCommitted.release();
if (mThread == null) {
mThread = new Thread(this);
mThread.start();
}
}
/** Returns an approximation of the bitrate of the RTP stream in bit per seconde. */
public long getBitrate() {
return mAverageBitrate.average();
}
/** Increments the sequence number. */
private void updateSequence() {
setLong(mBuffers[mBufferIn], ++mSeq, 2, 4);
}
/**
* Overwrites the timestamp in the packet.
* @param timestamp The new timestamp in ns.
**/
public void updateTimestamp(long timestamp) {
mTimestamps[mBufferIn] = timestamp;
setLong(mBuffers[mBufferIn], (timestamp/100L)*(mClock/1000L)/10000L, 4, 8);
}
/** Sets the marker in the RTP packet. */
public void markNextPacket() {
mBuffers[mBufferIn][1] |= 0x80;
}
/** The Thread sends the packets in the FIFO one by one at a constant rate. */
@Override
public void run() {
Statistics stats = new Statistics(50,3000);
try {
// Caches mCacheSize milliseconds of the stream in the FIFO.
Thread.sleep(mCacheSize);
long delta = 0;
while (mBufferCommitted.tryAcquire(4, TimeUnit.SECONDS)) {
if (mOldTimestamp != 0) {
// We use our knowledge of the clock rate of the stream and the difference between two timestamps to
// compute the time lapse that the packet represents.
if ((mTimestamps[mBufferOut]-mOldTimestamp)>0) {
stats.push(mTimestamps[mBufferOut]-mOldTimestamp);
long d = stats.average()/1000000;
//Log.d(TAG,"delay: "+d+" d: "+(mTimestamps[mBufferOut]-mOldTimestamp)/1000000);
// We ensure that packets are sent at a constant and suitable rate no matter how the RtpSocket is used.
if (mCacheSize>0) Thread.sleep(d);
} else if ((mTimestamps[mBufferOut]-mOldTimestamp)<0) {
Log.e(TAG, "TS: "+mTimestamps[mBufferOut]+" OLD: "+mOldTimestamp);
}
delta += mTimestamps[mBufferOut]-mOldTimestamp;
if (delta>500000000 || delta<0) {
//Log.d(TAG,"permits: "+mBufferCommitted.availablePermits());
delta = 0;
}
}
mReport.update(mPackets[mBufferOut].getLength(), System.nanoTime(),(mTimestamps[mBufferOut]/100L)*(mClock/1000L)/10000L);
mOldTimestamp = mTimestamps[mBufferOut];
if (mCount++>30) mSocket.send(mPackets[mBufferOut]);
if (++mBufferOut>=mBufferCount) mBufferOut = 0;
mBufferRequested.release();
}
} catch (Exception e) {
e.printStackTrace();
}
mThread = null;
resetFifo();
}
private void setLong(byte[] buffer, long n, int begin, int end) {
for (end--; end >= begin; end--) {
buffer[end] = (byte) (n % 256);
n >>= 8;
}
}
/**
* Computes an average bit rate.
**/
protected static class AverageBitrate {
private final static long RESOLUTION = 200;
private long mOldNow, mNow, mDelta;
private long[] mElapsed, mSum;
private int mCount, mIndex, mTotal;
private int mSize;
public AverageBitrate() {
mSize = 5000/((int)RESOLUTION);
reset();
}
public AverageBitrate(int delay) {
mSize = delay/((int)RESOLUTION);
reset();
}
public void reset() {
mSum = new long[mSize];
mElapsed = new long[mSize];
mNow = SystemClock.elapsedRealtime();
mOldNow = mNow;
mCount = 0;
mDelta = 0;
mTotal = 0;
mIndex = 0;
}
public void push(int length) {
mNow = SystemClock.elapsedRealtime();
if (mCount>0) {
mDelta += mNow - mOldNow;
mTotal += length;
if (mDelta>RESOLUTION) {
mSum[mIndex] = mTotal;
mTotal = 0;
mElapsed[mIndex] = mDelta;
mDelta = 0;
mIndex++;
if (mIndex>=mSize) mIndex = 0;
}
}
mOldNow = mNow;
mCount++;
}
public int average() {
long delta = 0, sum = 0;
for (int i=0;i<mSize;i++) {
sum += mSum[i];
delta += mElapsed[i];
}
//Log.d(TAG, "Time elapsed: "+delta);
return (int) (delta>0?8000*sum/delta:0);
}
}
/** Computes the proper rate at which packets are sent. */
protected static class Statistics {
public final static String TAG = "Statistics";
private int count=500, c = 0;
private float m = 0, q = 0;
private long elapsed = 0;
private long start = 0;
private long duration = 0;
private long period = 6000000000L;
private boolean initoffset = false;
public Statistics(int count, long period) {
this.count = count;
this.period = period*1000000L;
}
public void push(long value) {
duration += value;
elapsed += value;
if (elapsed>period) {
elapsed = 0;
long now = System.nanoTime();
if (!initoffset || (now - start < 0)) {
start = now;
duration = 0;
initoffset = true;
}
value -= (now - start) - duration;
//Log.d(TAG, "sum1: "+duration/1000000+" sum2: "+(now-start)/1000000+" drift: "+((now-start)-duration)/1000000+" v: "+value/1000000);
}
if (c<40) {
// We ignore the first 40 measured values because they may not be accurate
c++;
m = value;
} else {
m = (m*q+value)/(q+1);
if (q<count) q++;
}
}
public long average() {
long l = (long)m-2000000;
return l>0 ? l : 0;
}
}
}
以上代码节选自开源项目Spydroid,Spydroid是一款优秀的android版视频推流开源项目。有兴趣的朋友可以点击github地址:https://github.com/fyhertz/spydroid-ipcamera下载