目录
1.2.2 将得到的ByteBuffer组装成h246编码格
1 H264编码格式
经过上述编码过程后,我们得到ByteBuffer流。接下来还要将ByteBuffer流按照一定的规则组装成流文件。
h264的两种码流格式,分别为:字节流格式和RTP包格式。
(1)字节流格式:这是在h264官方协议文档中规定的格式,处于文档附录B(Annex-B Byte stream format)中。所以它也成为了大多数编码器,默认的输出格式。它的基本数据单位为NAL单元,也即NALU。为了从字节流中提取出NALU,协议规定,在每个NALU的前面加上起始码:0x000001或0x00000001(0x代表十六进制) 。
(2)RTP包格式:这种格式并没有在h264中规定,那为什么还要介绍它呢?是因为在h264的官方参考软件JM里,有这种封装格式的实现。在这种格式中,NALU并不需要起始码Start_Code来进行识别,而是在NALU开始的若干字节(1,2,4字节),代表NALU的长度。
1.1字节流格式
1.1.1 起始码与NALU
H264比特流 = Start_Code_Prefix + NALU + Start_Code_Prefix + NALU + …
Start_Code_Prefix 为起始码,值为0x000001或0x00000001。可以看出h264流就是起始码和NALU组装而成。
1.1.2 NALU
NALU = NALU Header + RBSP
可以看到,整个NALU语法元素分为三部分:(1)NALU Header、(3)RBSP、(2)1和3之间的部分。
其中第2部分,是近期的h264文档才更新的,这部分是还没有实现的,只有当nal_unit_type等于14、20、21时,才会进入第二部分。
所以接下来呢,我们就重点介绍NALU Header和RBSP。
1.1.2 NALU Header
通过上面我们也可以看到,NALU Header由三个句法元素组成,分别为:forbidden_zero_bit、nal_ref_idc和nal_unit_type,它们总共占据一个字节,也就是说,NALU Header,在整个NALU中,占据一个字节。
而且forbidden_zero_bit的值对应1个bit,nal_ref_idc的值对应2个bit,nal_unit_type的值对应5个bit,加起来刚好一个字节。
正如在上一篇(链接)中所介绍的,知道了句法元素,我们就来分别看看它的语义:
(1) forbidden_zero_bit
h264文档规定,这个值应该为0,当它不为0时,表示网络传输过程中,当前NALU中可能存在错误,解码器可以考虑不对这个NALU进行解码。
(2) nal_ref_idc
取值0~3,代表当前这个NALU的重要性,取值越大,代表当前NALU越重要,就需要优先被保护。尤其是当前NALU为图像参数集、序列参数集或IDR图像时,或者为参考图像条带(片/Slice),或者为参考图像的条带数据分割时,nal_ref_idc值肯定不为0。
而当NALU 类型,nal_unit_type为6、9、10、11、或12时,nal_ref_idc都为0。
【注】IDR帧,即:即时解码刷新图像,它是一个序列的第一个图像,H.264引入IDR图像是为了解码的重新同步。当解码器解码到IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样一来,如果前一个序列发生重大错误,在这里就可以获得重新同步。
所以IDR图像之后的图像,永远不会引用IDR图像之前的图像来解码。并且IDR图像一定是I图像,而I图像不一定是IDR图像(H264里没有图像层,图像可以理解为帧、片或宏块)。
(3) nal_unit_type
顾名思义,这个应该是最好理解的了,它表示NALU Header后面的RBSP的数据结构的类型。
nal_unit_type的值为1-5时,表示RBSP里面包含的数据为条带(片/Slice)数据,所以值为1-5的NALU统称为VCL(视像编码层)单元,其他的NALU则称为非VCL NAL单元。
当nal_unit_type为7时,代表当前NALU为序列参数集SPS,为8时为图像参数集PPS。这也是我们打开.h264文件后,遇到的前两个NALU,它们位于码流的最前面。
而且当nal_unit_type为14-31时,我们可以不用理睬,目前几乎用不到。
解析完NALU Header之后,下面就开始解析RBSP了,它包含了NALU数据的主体部分
可以看到,在nal_unit_type取不同值时,代表着不同类型的nalu数据。这里想要了解这些文件可参考文章:
从上述过程可以看出大概的组装流程,先是找到起始码,根据起始码找到NALU。取出NALU的第一个字节,根据后5bit位查看当前的NALU是哪一类,如果为7就是SPS,为8就是PPS,以此类推。再根据类型添加头文件和数据。
接下来,我们通过代码实例来看组装成字节流的具体过程。
1.2 代码实例
在Android中采用MediaCodec进行硬编码。至于软编码可以用ffmpeg,这是一个很强大的工具。在这里,我们只讨论硬编码。
想要进行硬编码,可以采用多种方式。比如可以用Surface和纹理(涉及到OpenGL ES)作为硬编码的输入,这又是一个很大的话题。当然我们也可以采用另一种比较简单的方法:将第2节的Byte数组作为输入给到MediaCodec。
1.2.1 硬编码
以视频编码为例,MediaCodec的使用方式为
//这里yuvFrame就是在上一章节中得到的像素byte数组。同理,如果是音频编码,传入的是音频数据
public void onProcessedYuvFrame(byte[] yuvFrame, int degree, boolean isFront, int colorFormat, int frameWidth, int frameHeight, long timeStamp) {
//获取编码器输入输出缓冲区
inBuffers = mLastEncoder.getInputBuffers();
outBuffers = mLastEncoder.getOutputBuffers();
//下面是将帧数据输入缓冲区
inBufferIndex = mLastEncoder.dequeueInputBuffer(0);
if (inBufferIndex >= 0) {
ByteBuffer bb = inBuffers[inBufferIndex];
bb.clear();
bb.put(yuvFrame, 0, yuvFrame.length);
long pts = mPresentTimeUs + timeStamp * 1000;
mLastEncoder.queueInputBuffer(inBufferIndex, 0, yuvFrame.length, pts, 0);
}
//下面是将编码后数据输出到输出缓冲区
int outBufferIndex = mLastEncoder.dequeueOutputBuffer(vebi, 0);
if (outBufferIndex >= 0) {
if (vebi.size > 0) {
long startfor = System.currentTimeMillis();
ByteBuffer bb = outBuffers[outBufferIndex];
bb.position(vebi.offset);
bb.limit(vebi.offset + vebi.size);
//将编码后数据组装成字节流格式
onEncodedAnnexbFrame(bb, vebi);
}
}
}
我们注意到,在mLastEncoder.queueInputBuffer中有一个参数是pts(Presentation Time Stamp)。音频编码中同样有一个pts,用来实现解码后音视频同步播放。
-
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
-
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
1.2.2 将得到的ByteBuffer组装成h246编码格
h264编码流程代码块
//bb为硬编码后的ByteBuffer数据
while (bb.position() < size) {
//这里在avc.annexb_demux找到NALU,下面是对NALU进行解析和组装
SrsFlvFrameBytes frame = avc.annexb_demux(bb, size);
// 5bits, 7.3.1 NAL unit syntax,
// H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
// 7: SPS, 8: PPS, 5: I Frame, 1: P Frame
//这里找到nal_unit_type的值
int nal_unit_type = (int) (frame.data.get(0) & 0x1f);
if (nal_unit_type == SrsAvcNaluType.SPS || nal_unit_type == SrsAvcNaluType.PPS) {
LogUtils.i(TAG, String.format("annexb demux %dB, pts=%d, frame=%dB, nalu=%d", size, pts, frame.size, nal_unit_type));
}
// for IDR frame, the frame is keyframe.
//判断nal_unit_type是否时IDR文件,也就是上图Fig NALU中类型为5
if (nal_unit_type == SrsAvcNaluType.IDR) {
frame_type = SrsCodecVideoAVCFrame.KeyFrame;
}
// ignore the nalu type aud(9)
if (nal_unit_type == SrsAvcNaluType.AccessUnitDelimiter) {
continue;
}
// for sps
//判断类型是否是sps,如果是就放入到h264_sps中,
//这个h264_sps在后面会被用来组装SPS帧
if (avc.is_sps(frame)) {
if (!frame.data.equals(h264_sps)) {
byte[] sps = new byte[frame.size];
frame.data.get(sps);
h264_sps_changed = true;
h264_sps = ByteBuffer.wrap(sps);
}
continue;
}
// for pps
if (avc.is_pps(frame)) {
if (!frame.data.equals(h264_pps)) {
byte[] pps = new byte[frame.size];
frame.data.get(pps);
h264_pps_changed = true;
h264_pps = ByteBuffer.wrap(pps);
}
continue;
}
// ibp frame.
//用来组装naluHeader
SrsFlvFrameBytes nalu_header = avc.mux_ibp_frame(frame);
//分别添加Header和数据
ibps.add(nalu_header);
ibps.add(frame);
}
write_h264_sps_pps(dts, pts);
write_h264_ipb_frame(ibps, frame_type, dts, pts);
}
以上为h264编码的大致流程。下面我们对一些流程分别看代码,比如查找起始码、组装SPS。
首先,查找NALU
//这里就对应上一个代码块“h264编码流程代码块”中annexb_demux 的方法,其中bb为编码后的ByteBuffer流
public SrsFlvFrameBytes annexb_demux(ByteBuffer bb, int size) throws IllegalArgumentException {
SrsFlvFrameBytes tbb = new SrsFlvFrameBytes();
while (bb.position() < size) {
// each frame must prefixed by annexb format.
// about annexb, @see H.264-AVC-ISO_IEC_14496-10.pdf, page 211.
//这里对000001进行查找,也就是当前NALU的起始码,这个起始码标志着NALU的开始
SrsAnnexbSearch tbbsc = utils.srs_avc_startswith_annexb(bb, size);
if (!tbbsc.match || tbbsc.nb_start_code < 3) {
LogUtils.e(TAG, "annexb not match match: "+tbbsc.match);
LogUtils.e(TAG, "annexb not match nb_start_code: "+tbbsc.nb_start_code);
SrsFlvMuxer.srs_print_bytes(TAG, bb, 16);
throw new IllegalArgumentException(String.format("annexb not match for %dB, pos=%d", size, bb.position()));
}
// the start codes.
ByteBuffer tbbs = bb.slice();
for (int i = 0; i < tbbsc.nb_start_code; i++) {
bb.get();
}
// find out the frame size.
tbb.data = bb.slice();
int pos = bb.position();
//找到下一个起始码之前的NALU数据的范围,也就是当前的NALU的数据范围
while (bb.position() < size) {
SrsAnnexbSearch bsc = utils.srs_avc_startswith_annexb(bb, size);
if (bsc.match) {
break;
}
bb.get();
}
//找到并计算长度
tbb.size = bb.position() - pos;
if (bb.position() < size) {
LogUtils.i(TAG, String.format("annexb multiple match ok, size=%dB", size));
SrsFlvMuxer.srs_print_bytes(TAG, tbbs, 16);
SrsFlvMuxer.srs_print_bytes(TAG, bb.slice(), 16);
}
break;
}
return tbb;
}
}
在组装nalu之前,先定义了nalu相关属性,这里只定义了NALU长度。注意这里nalu_header不同于3.3.2中naluHeader的定义,3.3.2节中naluHeader是nalu内部的头部文件,这里的nalu_header是用来定义视频帧nalu长度属性的。
//如果是视频帧数据,定义这个帧数据naluHeader
public SrsFlvFrameBytes mux_ibp_frame(SrsFlvFrameBytes frame) {
//定义NALUHeader
SrsFlvFrameBytes nalu_header = new SrsFlvFrameBytes();
//定义有几个字节存放NALU的长度数据
nalu_header.size = 4;
nalu_header.data = ByteBuffer.allocate(nalu_header.size);
// 5.3.4.2.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
// lengthSi*usOne, or NAL_unit_length, always use 4bytes size
int NAL_unit_length = frame.size;
// mux the avc NALU in "ISO Base Media File Format"
// from H.264-AVC-ISO_IEC_14496-15.pdf, page 20
// NALUnitLength
nalu_header.data.putInt(NAL_unit_length);
// reset the buffer.
nalu_header.data.rewind();
return nalu_header;
}
以上就是组装视频帧nalu过程
接下来,写入sps,pps。
在1.2.2节的代码中,可以看到,如果nal_unit_type是SPS和PPS的标记(即7或者8)时,单独分别放入到h264_sps,h264_pps。接下来,将sps和pps的nalu写入
public void mux_sequence_header(ByteBuffer sps, ByteBuffer pps, int dts, int pts, ArrayList<SrsFlvFrameBytes> frames) {
// 5bytes sps/pps header:
// configurationVersion, AVCProfileIndication, profile_compatibility,
// AVCLevelIndication, lengthSi*usOne
// 3bytes size of sps:
// numOfSequenceParameterSets, sequenceParameterSetLength(2B)
// Nbytes of sps.
// sequenceParameterSetNALUnit
// 3bytes size of pps:
// numOfPictureParameterSets, pictureParameterSetLength
// Nbytes of pps:
// pictureParameterSetNALUnit
// decode the SPS:
// @see: 7.3.2.1.1, H.264-AVC-ISO_IEC_14496-10-2012.pdf, page 62
if (true) {
SrsFlvFrameBytes hdr = new SrsFlvFrameBytes();
hdr.size = 5;
hdr.data = ByteBuffer.allocate(hdr.size);
// @see: Annex A Profiles and levels, H.264-AVC-ISO_IEC_14496-10.pdf, page 205
// Baseline profile profile_idc is 66(0x42).
// Main profile profile_idc is 77(0x4d).
// Extended profile profile_idc is 88(0x58).
byte profile_idc = sps.get(1);
//u_int8_t constraint_set = frame[2];
byte level_idc = sps.get(3);
// generate the sps/pps header
// 5.3.4.2.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
// configurationVersion
hdr.data.put((byte) 0x01);
// AVCProfileIndication
hdr.data.put(profile_idc);
// profile_compatibility
hdr.data.put((byte) 0x00);
// AVCLevelIndication
hdr.data.put(level_idc);
// lengthSi*usOne, or NAL_unit_length, always use 4bytes size,
// so we always set it to 0x03.
hdr.data.put((byte) 0x03);
// reset the buffer.
hdr.data.rewind();
frames.add(hdr);
}
// sps
if (true) {
SrsFlvFrameBytes sps_hdr = new SrsFlvFrameBytes();
sps_hdr.size = 3;
sps_hdr.data = ByteBuffer.allocate(sps_hdr.size);
// 5.3.4.2.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
// numOfSequenceParameterSets, always 1
sps_hdr.data.put((byte) 0x01);
// sequenceParameterSetLength
sps_hdr.data.putShort((short) sps.array().length);
sps_hdr.data.rewind();
frames.add(sps_hdr);
// sequenceParameterSetNALUnit
SrsFlvFrameBytes sps_bb = new SrsFlvFrameBytes();
sps_bb.size = sps.array().length;
sps_bb.data = sps.duplicate();
frames.add(sps_bb);
}
// pps
if (true) {
SrsFlvFrameBytes pps_hdr = new SrsFlvFrameBytes();
pps_hdr.size = 3;
pps_hdr.data = ByteBuffer.allocate(pps_hdr.size);
// 5.3.4.2.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
// numOfPictureParameterSets, always 1
pps_hdr.data.put((byte) 0x01);
// pictureParameterSetLength
pps_hdr.data.putShort((short) pps.array().length);
pps_hdr.data.rewind();
frames.add(pps_hdr);
// pictureParameterSetNALUnit
SrsFlvFrameBytes pps_bb = new SrsFlvFrameBytes();
pps_bb.size = pps.array().length;
pps_bb.data = pps.duplicate();
frames.add(pps_bb);
}
}
添加sps和pps代码中,可以看到添加了header和sps数据或者pps数据。
上述过程是对视频进行h264格式编码,对音频进行aac格式编码也是相似的。
2 封装
上一章节中实现了将视频数据和音频数据分别进行编码压缩。像是在搬家时将所有的东西进行打包,在将那些非常占空间的物件进行真空压缩(相当于压缩编码)后,同样需要将这些经过压缩后的东西“打包”,放在直播中就是将压缩后的音视频数据进行“封装”。
类比于一个包裹有着“寄件人、收件人、重量、收货地址,寄件种类”等属性,通过收货地址,物流公司可以知道要送到哪里去,通过寄件种类,可以知道寄的是哪一类物品。音视频数据封装就是给这些音视频帧数据也添加一些属性,以实现后端和播放端拿到信息和音视频数据,并根据这些属性或者数据进行下一步的动作。
现存有多种封装格式,flv的封装格式是怎样的呢?
flv封装音视频和h265组装方式相似,想要了解flv的格式可以参考文章