将NALU封装成RTP包
源码地址: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的最后一个单元。
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格式如下:
另外几个字段直接使用传进来的参数即可。
bb.putShort((short) seqNum);
bb.putInt(timestamp);
bb.putInt(ssrc);
bb.put(pcData);
二、NALU长度>MTU
另一种情况就是,NALU长度>MTU。
首先是NALU格式:
可以看出,NALU的头部只有一个字节,但是RTP的分片封包模式除了一个字节的FU header外,还有一个字节的FU Indicator,需要我们手动构造出来。
[RTP header]+[FU Indicator(1字节)]+[FU header(1字节)]+[部分nalu payload]
首先从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中。