直播推流SDK综述(二)

目录

1 H264编码格式

1.1字节流格式

1.1.1  起始码与NALU

1.1.2 NALU

1.1.3 NALU Header

1.1.4 SPS组成

1.2 代码实例

1.2.1 硬编码

1.2.2 将得到的ByteBuffer组装成h246编码格式

2 封装


在这篇文章里呢,我们就先从宏观的角度,来看看使用H.264编码之后,得到的裸流的分层结构是什么样的。

H.264的比特流,是计算机能理解的二进制语言,比如101111000101011111000(当然是瞎写的),它可能就在表达图像的宽或高、片的个数、宏块的大小、样点像素值等等。

当然具体在表达什么,我们需要学习了H.264的句法和语义之后才知道,所以学习H.264的句法和语义。

H.264的分层结构,它分别为序列、图像、片、宏块、子宏块这5个层次。

当然有的同学一下并不能完全理解,句法元素的分层结构是指什么,那是因为要想完全理解,还要结合之后要讲的NALU,以及序列、图像、片、宏块、子宏块这些层次.在这里,我们可以先简单的理解为,图像引用序列,片引用图像,同时片包含宏块,宏块包含子宏块。

直播推流SDK综述(二)_第1张图片

接下来,我们会初步具备解析h264码流的能力,从码流中分离出NAL单元,并识别NAL类型。

1 H264编码格式

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的长度。

这里我们只讨论字节流格式。对于一些基本概念,可参考文章:https://www.jianshu.com/p/6fe07c318222

1.1字节流格式

 在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。因此我们平时的每帧数据就是一个NAL单元(SPS与PPS除外)。在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符,一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧……

这里我们讨论的是网络抽象层面。对于视频编码层面的原理可以参考上篇文章第二个章节:https://blog.csdn.net/murongxian_1/article/details/111656161

1.1.1  起始码与NALU

h264的比特流组成为:

H264比特流 = Start_Code_Prefix + NALU + Start_Code_Prefix + NALU + …

Start_Code_Prefix 为起始码,值为0x000001或0x00000001。可以看出h264流就是起始码和NALU组装而成。

1.1.2 NALU

直播推流SDK综述(二)_第2张图片

NALU = NALU Header + RBSP

直播推流SDK综述(二)_第3张图片

可以看到,整个NALU语法元素分为三部分:(1)NALU Header、(3)RBSP、(2)1和3之间的部分。

其中第2部分,是近期的h264文档才更新的,这部分是还没有实现的,只有当nal_unit_type等于14、20、21时,才会进入第二部分。

所以接下来呢,我们就重点介绍NALU Header和RBSP。

1.1.3 NALU Header

直播推流SDK综述(二)_第4张图片

我们来层层分析,以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,解读顺序为从左往右算,如下:
1)第1位禁止位,值为1表示语法出错
2)第2~3位为参考级别
3)第4~8为是nal单元类型

通过上面我们也可以看到,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数据的主体部分

直播推流SDK综述(二)_第5张图片 Fig NALU​​​​

可以看到,在nal_unit_type取不同值时,代表着不同类型的nalu数据。除了音视频数据帧,还有比较重要的就是sps、pps,它们保存着视频参数等重要信息。

例如上面00000001后有67,68以及65

其中0x67的二进制码为:

0110 0111

4-8为00111,转为十进制7,参考第二幅图:7对应序列参数集SPS

其中0x68的二进制码为:

0110 1000

4-8为01000,转为十进制8,参考第二幅图:8对应图像参数集PPS

其中0x65的二进制码为:

0110 0101

4-8为00101,转为十进制5,参考第二幅图:5对应IDR图像中的片(I帧)

所以判断是否为I帧的算法为: (NALU类型  & 0001  1111)= 5   即   NALU类型  & 31 = 5

比如0x65 & 31 = 5

从上述过程可以看出大概的组装流程,先是找到起始码,根据起始码找到NALU。取出NALU的第一个字节,根据后5bit位查看当前的NALU是哪一类,如果为7就是SPS,为8就是PPS,以此类推。再根据类型添加头文件和数据。

本质上来说,nalu其实就是h246协议定义的发送数据的具体格式。发送端对编码好的数据按照h246定义的流的要求进行组装,接收端再按照h246的格式进行解码,就能保证流程的正确进行。

1.1.4 SPS组成

  在H.264标准协议中规定了多种不同的NAL Unit类型,其中类型7表示该NAL Unit内保存的数据为Sequence Paramater Set。在H.264的各种语法元素中,SPS中的信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。SPS及后续将要讲述的图像参数集PPS在某些平台的视频处理框架(比如iOS的VideoToolBox等)还通常作为解码器实例的初始化信息使用。

     SPS即Sequence Paramater Set,又称作序列参数集。SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS和PPS的NAL Unit通常位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:

  • 解码器需要在码流中间开始解码;
  • 编码器在编码的过程中改变了码流的参数(如图像分辨率等);

在做视频播放器时,为了让后续的解码过程可以使用SPS中包含的参数,必须对其中的数据进行解析。其中H.264标准协议中规定的SPS格式位于文档的7.3.2.1.1部分,如下图所示: 
 

其中的每一个语法元素及其含义如下:

(1) profile_idc:

标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:

基准档次:baseline profile;

主要档次:main profile;

扩展档次:extended profile;

在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:

profile_idc = 66 → baseline profile;

profile_idc = 77 → main profile;

profile_idc = 88 → extended profile;

在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。

另外,constraint_set0_flag ~ constraint_set5_flag是在编码的档次方面对码流增加的其他一些额外限制性条件。

在我们实验码流中,profile_idc = 0x42 = 66,因此码流的档次为baseline profile。

(2) level_idc

标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。

当前码流中,level_idc = 0x1e = 30,因此码流的级别为3。

(3) seq_parameter_set_id

表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。

(4) log2_max_frame_num_minus4

用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 + 4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。

(5) pic_order_cnt_type

表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。

(6) log2_max_pic_order_cnt_lsb_minus4

用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为MaxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_minus4 + 4)。

(7) max_num_ref_frames

用于表示参考帧的最大数目。

(8) gaps_in_frame_num_value_allowed_flag

标识位,说明frame_num中是否允许不连续的值。

(9) pic_width_in_mbs_minus1

用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:

frame_width = 16 × (pic\_width\_in\_mbs_minus1 + 1);

(10) pic_height_in_map_units_minus1

使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:

PicHeightInMapUnits = pic\_height\_in\_map\_units\_minus1 + 1;

(11) frame_mbs_only_flag

标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。

按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:

FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits

(12) mb_adaptive_frame_field_flag

标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。

(13) direct_8x8_inference_flag

标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。

(14) frame_cropping_flag

标识位,说明是否需要对输出的图像帧进行裁剪。

(15) vui_parameters_present_flag

标识位,说明SPS中是否存在VUI信息。

除了SPS信息,还可以在这个链接中查看PPS信息:

https://blog.csdn.net/DaveBobo/article/details/75041348?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control

当然还有其他的一些码流信息,关于h264格式具体内容可以查看:http://www.itu.int/rec/T-REC-H.264

接下来,我们通过代码实例来看硬编码后组装成字节流的大致过程以及查找起始码和SPS编码的具体过程。

1.2 代码实例

在Android中采用MediaCodec进行硬编码。至于软编码可以用ffmpeg,这是一个很强大的工具。在这里,我们只讨论硬编码。

想要进行硬编码,可以采用多种方式。比如可以用Surface和纹理(涉及到OpenGL ES)作为硬编码的输入,这又是一个很大的话题,有兴趣可以自己查查资料,美颜特效之类的就是通过OpenGL实现的。当然我们也可以采用另一种比较简单的方法:将第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):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

硬编码结束,我们就拿到了经过帧内编码、真贱编码、离散余弦变换、CABAC压缩后的数据。接下来就是将这些数据组装成h264的编码格式。

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相关属性,这里用SrsFlvFrameBytes对象定义了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
            // lengthSizeMinusOne, 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;
        }

接下来,写入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 frames) {
            // 5bytes sps/pps header:
            //      configurationVersion, AVCProfileIndication, profile_compatibility,
            //      AVCLevelIndication, lengthSizeMinusOne
            // 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);
                // lengthSizeMinusOne, 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数据。为简单起见,上述代码中之添加了一些关于SPS/PPS Header的必要信息。

其他nal_unit_type的组装也是相似的。

2 封装

上一章节中实现了将视频数据和音频数据分别进行编码压缩。像是在搬家时将所有的东西进行打包,在将那些非常占空间的物件进行真空压缩(相当于压缩编码)后,同样需要将这些经过压缩后的东西“打包”,放在直播中就是将压缩后的音视频数据进行“封装”。

类比于一个包裹有着“寄件人、收件人、重量、收货地址,寄件种类”等属性,通过收货地址,物流公司可以知道要送到哪里去,通过寄件种类,可以知道寄的是哪一类物品。音视频数据封装就是给这些音视频帧数据也添加一些属性,以实现后端和播放端拿到信息和音视频数据,并根据这些属性或者数据进行下一步的动作。

现存有多种封装格式,flv的封装格式是怎样的呢?

flv封装音视频和h265组装方式相似,想要了解flv的格式可以参考文章

https://blog.csdn.net/yangzm/article/details/43731719?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4.control

 

参考文章:https://blog.csdn.net/special00/article/details/82533768

https://www.jianshu.com/p/6fe07c318222

https://blog.csdn.net/special00/article/details/82533768

http://www.itu.int/rec/T-REC-H.264

https://blog.csdn.net/DaveBobo/article/details/75041348?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.control

 


 

你可能感兴趣的:(音视频,h264,android,java)