欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

将NALU封装成RTP包

程序员文章站 2022-07-14 18:45:55
...

源码地址:https://github.com/zhouyinfei/rtsp-netty-server

 

NALU封装成RTP包:

//nalu封装成rtp
public List<byte[]> naluToRtpPack(byte[] nalu, int ssrc, int fps){
	
	byte[] pcData = nalu;						//两个起始码(00 00 00 01)之间的NALU数据
	int mtu = 1400;								//最大传输单元
	int iLen = pcData.length;					//NALU总长度
	ByteBuffer bb = null;					
	List<byte[]> rtpList = new ArrayList<byte[]>();				//封装后的rtp包集合
	
	if (iLen > mtu) { //超过MTU						分片封包模式
		byte start_flag = (byte) 0x80;
		byte mid_flag = 0x00;
		byte end_flag = 0x40;
		
		//获取帧头数据,1byte
		byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
		byte nal_ref_idc = (byte) (pcData[0] & 0x60); //获取NALU的2bit 帧重要程度 00 可以丢 11不能丢
		
		//组装FU-A帧头数据 2byte
		byte fu_identifier = (byte) (nal_ref_idc + 28);			//F为0 1bit,nri上面获取到2bit,28为FU-A分片类型5bit
		byte fu_header = nalu_type;								//低5位和naluHeader相同
		boolean bFirst = true;									//是否是第一个分片
		boolean mark = false;									//是否是最后一个分片
		int nOffset = 1;										//偏移量,跳过第一个字节naluHeader
		while (!mark) {
			bb = ByteBuffer.allocate(mtu + 2);
			if (iLen < nOffset + mtu) {           //是否拆分结束, 最后一个分片
				mtu = iLen - nOffset;
				
				mark = true;
				fu_header = (byte) (end_flag + nalu_type);		
			} else {				
				if (bFirst == true) {					//第一个分片
					fu_header = (byte) (start_flag + nalu_type);
					bFirst = false;
				} else {								//或者中间分片
					fu_header = (byte) (mid_flag + nalu_type);
				}
			}
			bb.put(fu_identifier);
			bb.put(fu_header);
			byte[] dest = new byte[mtu];
			System.arraycopy(pcData, nOffset, dest, 0, mtu);
			bb.put(dest);
			nOffset += mtu;
			byte[] rtpPackage = makeH264Rtp(bb.array(), mark, seqNum, (int)System.currentTimeMillis(), ssrc);
			rtpList.add(rtpPackage);
			seqNum ++;
		}
	} else {				//单一NAL 单元模式, 不使用组合模式。mark始终为false
		//从SPS中获取帧率
//	    	byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
//	    	if (nalu_type == 7) {				//如果是sps
//				SpsInfoStruct info = new SpsInfoStruct();
//				SpsUtils.h264ParseSps(nalu, nalu.length, info);
//				if (info.fps != 0) {			//如果sps中存在帧率信息,则使用。如果没有,则使用默认帧率
//					intervel = 1000/info.fps;		
//				}
//			}
		
		//根据rtsp传过来的fps参数
		if (fps != 0) {
			intervel = 1000/fps;	
		}
		
		byte[] rtpPackage = makeH264Rtp(pcData, false, seqNum, (int)System.currentTimeMillis(), ssrc);
		rtpList.add(rtpPackage);
		seqNum ++;
	}
	
	try {
		Thread.sleep(intervel);					//根据帧率延时发送
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return rtpList;
}


public byte[] makeH264Rtp(byte[] pcData, boolean mark, int seqNum, int timestamp, int ssrc){
	byte b;
	if (mark) {
		b = (byte) 0xE0;	
	} else {
		b = (byte) 0x60;
	}
	
	ByteBuffer bb = ByteBuffer.allocate(pcData.length + 12);
	bb.put((byte) 0x80);				//V、P、X、CC, 1000 0000
	bb.put(b);							//mark 、payloadType(96)
	bb.putShort((short) seqNum);
	bb.putInt(timestamp);
	bb.putInt(ssrc);				
	bb.put(pcData);
	return bb.array();
}

就跟RTP中解析H264的过程类似,将H264文件封装成RTP包时同样要考虑分片封包的情况。只不过这里为了简单处理,当NALU长度大于MTU时,会采取分片封包模式。但是当NALU长度小于MTU很多时,直接将一个NALU封装成一个RTP包,而不是采取组合封包模式。

 

public List<byte[]> naluToRtpPack(byte[] nalu, int ssrc, int fps)

其中,参数中nalu,指的是两个起始码(00 00 00 01)之间的数据。这里有两种情况,一种是大于MTU,一种是小于或等于MTU。

 

一、NALU长度<= MTU

先从比较简单的情况开始:NALU长度<= MTU

byte[] rtpPackage = makeH264Rtp(pcData, false, seqNum, (int)System.currentTimeMillis(), ssrc);
rtpList.add(rtpPackage);
seqNum ++;

NALU和RTP包是一一对应的关系。调用了makeH264Rtp方法,其中mark参数表示该RTP包是否是NALU的最后一个单元。

将NALU封装成RTP包

if (mark) {
    b = (byte) 0xE0;    
} else {
    b = (byte) 0x60;
}

 b是RTP头的第二个字节,假定我们将H264的payloadType定为96(0110 0000),那么根据RTP的结构,mark为true时,b=1110 0000(即0xE0),而mark为false时,b=0110 0000(即0x60)。

RTP格式如下:

将NALU封装成RTP包

另外几个字段直接使用传进来的参数即可。

bb.putShort((short) seqNum);
bb.putInt(timestamp);
bb.putInt(ssrc);                
bb.put(pcData);

 

二、NALU长度>MTU

另一种情况就是,NALU长度>MTU。 

首先是NALU格式:

将NALU封装成RTP包

可以看出,NALU的头部只有一个字节,但是RTP的分片封包模式除了一个字节的FU header外,还有一个字节的FU Indicator,需要我们手动构造出来。

[RTP header]+[FU Indicator(1字节)]+[FU header(1字节)]+[部分nalu payload]

将NALU封装成RTP包

首先从NALU的头部获取出有用的信息:nalu_type(后5位)和NRI(中间2位):

 //获取帧头数据,1byte
byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
byte nal_ref_idc = (byte) (pcData[0] & 0x60); //获取NALU的2bit 帧重要程度 00 可以丢 11不能丢

 FU Indicator的NRI字段和NALU的一样,但是后5位的取值是28(FU-A),因此它的值为:

byte fu_identifier = (byte) (nal_ref_idc + 28);            //F为0 1bit,nri上面获取到2bit,28为FU-A分片类型5bit

 FU header的后5位和NALU类型一样,但是它的前2位表示起始分片和最后一个分片的标识位,因此在不同情况下是不同的:

byte fu_header = nalu_type;                                //低5位和naluHeader相同

当起始分片时:

fu_header = (byte) (start_flag + nalu_type);

当中间分片时:

fu_header = (byte) (mid_flag + nalu_type);

当最后一个分片时:

fu_header = (byte) (end_flag + nalu_type);    

 

在解析NALU的过程中,需要设置几个标志,表示是否是第一个分片、是否最后一个分片、以及当前解析到哪个位置。

boolean bFirst = true;                                    //是否是第一个分片
boolean mark = false;                                    //是否是最后一个分片
int nOffset = 1;                                        //偏移量,跳过第一个字节naluHeader

 然后每次都读取MTU个字节,直到末尾。最后一次只读取iLen - nOffset个字节,然后将这些数据依照前面的规则封装成RTP,因为有多个RTP包,因此存入到一个List中。

 

相关标签: 音视频开发