直播推流SDK综述

目录

1 H264编码格式

1.1字节流格式

1.1.1  起始码与NALU

1.1.2 NALU Header

1.2 代码实例

1.2.1 硬编码

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

2 封装


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

直播推流SDK综述

NALU = NALU Header + RBSP

直播推流SDK综述

可以看到,整个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数据的主体部分

直播推流SDK综述
Fig NALU​​​​

可以看到,在nal_unit_type取不同值时,代表着不同类型的nalu数据。这里想要了解这些文件可参考文章:

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

从上述过程可以看出大概的组装流程,先是找到起始码,根据起始码找到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的格式可以参考文章

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

 

 


 

上一篇:目前无法使你登录 Microsoft Edge 团队已通知此问题。请稍后再试。 错误代码:3、15、1067


下一篇:CTFHUB-HTTP协议-基础认证(通过字典爆破解决)