H265(HEVC) nal 单元头介绍及rtp发送中的fu分组发送详解

对H.264编码标准一直停留在理解原理的基础上,对于一个实际投入使用的编码器是如何构建起来一直感觉很神秘,于是决定在理解理论的基础上潜心于编码器实现框架。关于开源的H264编码器有很多,JMVC,T264、X264,这里选择X264,因为网上关于X264源码分析资源很多。X264编码器是一个开源的经过优化的高性能H.264编码器,目前最新的源码在本人的I5处理器的PC机上,编码1920x1080分辨率的视频序列在使用ultrafast配置的情况下,可以实现160fps左右的编码速度。

这里对源码分析并没有选择最新的源码,而是选用2009年2月份的版本,原因有二:一是这个版本是从网上下载下来的,已经是一个建立好的VS2008工程,对于像我这种用惯IDE调试的程序员来说,这可以大大提高源码阅读速度;二是虽然X264源码虽然几乎每天都有更新,但是从这个版本以后,最大的改动基本是针对多Slice编码的支持,其他都是在输入输出方面的一些改动,从这个版本学习可以较快进入编码部分的学习;三是这个版本当中已经有别人的很多的注释,便于自己理解;

一般将编码分为帧级、片级、宏块级编码,依次从上到下。下面就从这三个级分析X264编码流程:

一、帧级编码分析

定位到x264_encoder_encode这个函数,这个函数应该是H264编码最上层的函数,实现编码一帧视频。在进行下一步分析之前有必要了解,控制X264编码的全局性结构体x264_t,这个结构体控制着视频一帧一帧的编码,包括中间参考帧管理、码率控制、全局参数等一些重要参数和结构体。
下面是x264_t这个结构体的定义(这里仅对几个关键的结构和变量进行分析):


     
     
  1. struct x264_t
  2. {
  3. x264_param_t param; //编码器编码参数,包括量化步长、编码级别等等一些参数
  4. ………..
  5. int i_frame; //编码帧号,用于计算POC(picture of count 标识视频帧的解码顺序)
  6. ………..
  7. int i_nal_type; /* Nal 单元的类型,可以查看编码标准,有哪几种类型,需要理解的类型有:不分区(一帧作为一个片)非IDR图像的片;片分区A、片分区B、片分区C、IDR图像中的片、序列参数集、图像参数集 */
  8. int i_nal_ref_idc; /* Nal 单元的优先级别 取值范围[0,1,2,3],值越大表示优先级越高,此Nal单元就越重要 */
  9. /* We use only one SPS(序列参数集) and one PPS(图像参数集) */
  10. x264_sps_t sps_array[ 1]; //结构体的数组
  11. x264_sps_t *sps;
  12. x264_pps_t pps_array[ 1];
  13. x264_pps_t *pps;
  14. int i_idr_pic_id;
  15. ……
  16. struct
  17. {
  18. //这个结构体涉及到X264编码过程中的帧管理,理解这个结构体中的变量在编码标准的理论意义是非常重要的
  19. x264_frame_t *current[X264_BFRAME_MAX* 4+ 3]; /*已确定帧类型,待编码帧,每一个GOP在编码前,每一帧的类型在编码前已经确定。当进行编码时,从这里取出一帧数据。*/
  20. x264_frame_t *next[X264_BFRAME_MAX* 4+ 3]; //尚未确定帧类型的待编码帧,当确定后,会将此数组中的帧转移到current数组中去。
  21. x264_frame_t *unused[X264_BFRAME_MAX* 4 + X264_THREAD_MAX* 2 + 16+ 4]; /*这个数组用于回收那些在编码中分配的frame空间,当有新的需要时,直接拿过来用,不用重新分配新的空间,提高效率*/
  22. /* For adaptive B decision */
  23. x264_frame_t *last_nonb;
  24. /* frames used for reference + sentinels */
  25. x264_frame_t *reference[ 16+ 2]; //参考帧队列,注意参考帧都是重建帧
  26. int i_last_idr; /* 上一次刷新关键帧的帧号,配合前面的i_frame,可以用来计算POC */
  27. int i_input; /* Number of input frames already accepted */ //frames结构体中i_input指示当前输入的帧的(播放顺序)序号。
  28. int i_max_dpb; /* 分配解码图像缓冲的最大数量(DPB) */
  29. int i_max_ref0; //最大前向参考帧数量
  30. int i_max_ref1; //最大后向参考帧数量
  31. int i_delay; /* Number of frames buffered for B reordering */
  32. //i_delay设置为由B帧个数(线程个数)确定的帧缓冲延迟,在多线程情况下为i_delay = i_bframe + i_threads - 1。
  33. //而判断B帧缓冲填充是否足够则通过条件判断:h->frames.i_input <= h->frames.i_delay + 1 - h->param.i_threads。
  34. int b_have_lowres; /* Whether 1/2 resolution luma planes are being used */
  35. int b_have_sub8x8_esa;
  36. } frames; //指示和控制帧编码过程的结构
  37. /* current frame being encoded */
  38. x264_frame_t *fenc; //指向当前编码帧
  39. /* frame being reconstructed */
  40. x264_frame_t *fdec; //指向当前重建帧,重建帧的帧号要比当前编码帧的帧号小1
  41. /* references lists */
  42. int i_ref0; //前向参考帧的数量
  43. x264_frame_t *fref0[ 16+ 3]; /* 存放前向参考帧的数组(注意参考帧均是重建帧) */
  44. int i_ref1; //后向参考帧的数量
  45. x264_frame_t *fref1[ 16+ 3]; /* 存放后向参考帧的数组*/
  46. int b_ref_reorder[ 2];
  47. ……..
  48. };

定位到x264_encoder_encode这个函数,这个函数应该是H264编码最上层的函数,实现编码的帧级处理(如何进行参考帧管理、帧类型确定等等)。
 下面对x264_encoder_encode中几个关键函数以及关键部分进行分析:
1、x264_reference_update这个函数主要完成参考帧的更新,H.264的帧间预测需要使用参考帧,参考帧使用的都是已编码后的重建帧,每编码一帧的同时会重建此帧作为参考帧,在编码下一帧时,将此重建帧加入到参考帧队列中。函数实现如下:

     
     
  1. static inline void x264_reference_update( x264_t *h )
  2. {
  3. int i;
  4. if( h->fdec->i_frame >= 0 ) //重建帧帧数大于等于零时
  5. h->i_frame++; //当前编码帧的帧号要比重建帧的帧号大1
  6. if( !h->fdec->b_kept_as_ref ) /*如果重建帧不作为参考帧(不作为参考帧,当然不用加入参考帧队列了)*/
  7. { //when b frame is not used as reference frame
  8. if( h->param.i_threads > 1 )
  9. {
  10. x264_frame_push_unused( h, h->fdec );
  11. h->fdec = x264_frame_pop_unused( h );
  12. }
  13. return; //if b-frame is not used as reference, return
  14. }
  15. /* move lowres(低分辨率) copy of the image to the ref frame */
  16. for( i = 0; i < 4; i++)
  17. { /*暂时还不知道干嘛的*/
  18. XCHG( uint8_t*, h->fdec->lowres[i], h->fenc->lowres[i] );
  19. XCHG( uint8_t*, h->fdec->buffer_lowres[i], h->fenc->buffer_lowres[i] );
  20. }
  21. /* adaptive B decision needs a pointer, since it can't use the ref lists */
  22. if( h->sh.i_type != SLICE_TYPE_B )
  23. h->frames.last_nonb = h->fdec;
  24. /* move frame in the buffer */
  25. x264_frame_push( h->frames.reference, h->fdec ); /*把重建帧放入参考队列中*/
  26. if( h->frames.reference[h->frames.i_max_dpb] ) /*如果参考帧的个数大于解码图像缓存的最大数(decoded picture buffer(DPB))*/
  27. x264_frame_push_unused( h, x264_frame_shift( h->frames.reference ) ); /*取出参考队列中第一个参考重建帧,并放入暂时不用帧队列中*/
  28. h->fdec = x264_frame_pop_unused( h ); /*从暂时不用帧队列中,取出一帧作为新的重建帧buf*/
  29. }
2、帧排序部分,在H.264标准中采用编码顺序与显示顺序不同的编码方式,对于一个已经确定帧类型的待编码序列:IBBPBBP在编码时需要先排序为IPBBPBB,然后进行编码。在X264代码中,实现在如下部分:

     
     
  1. //确定帧的类型
  2. x264_stack_align( x264_slicetype_decide, h ); /*通过x264_slicetype_decide函数来确定决定h-frames.next[]中每一帧的类型*/
  3. /* 3: move some B-frames and 1 non-B to encode queue 这里来完成帧排序,还是有点巧妙的*/
  4. while( IS_X264_TYPE_B( h->frames.next[bframes]->i_type ) )
  5. bframes++; /*注意这个循环查找的作用,一方面可以确定第一个非B帧之前B帧的数量,也可以定位出第一个非B帧的位置*/
  6. x264_frame_push( h->frames.current, x264_frame_shift( &h->frames.next[bframes] ) ); /*取出第一个非B帧,并放到current指针第一个位置:通过这两步完成帧排序的(例如BBP->PBB)*/
  7. /* FIXME: when max B-frames > 3, BREF may no longer be centered after GOP closing */
  8. if( h->param.b_bframe_pyramid && bframes > 1 )
  9. {
  10. x264_frame_t *mid = x264_frame_shift( &h->frames.next[bframes/ 2] );
  11. mid->i_type = X264_TYPE_BREF;
  12. x264_frame_push( h->frames.current, mid );
  13. bframes--;
  14. }
  15. while( bframes-- )
  16. x264_frame_push( h->frames.current, x264_frame_shift( h->frames.next ) ); /*然后依次取出B帧,并放到current队列中*/
下面就可以从h->frames.current队列中取出第一帧放入h->fenc中
h->fenc = x264_frame_shift( h->frames.current );//从当前编码帧中取出第一帧,作为当前编码帧
     
     
然后就开始编码,首先当前编码帧(h->fenc)的类型,设定slice类型,这里就不解释了,关于IDR帧,执行了x264_reference_reset这个函数将参考帧队列清空。
接着进行相关参数的赋值,这里主要对POC的计算强调一下:
h->fenc->i_poc = 2 * (h->fenc->i_frame - h->frames.i_last_idr);//考虑到场编码,POC每帧增长为2,如果是场编码POC增长为1
     
     
3、重建参考帧列表(x264_reference_build_list),即将参考帧列表中的参考帧分为前向参考帧和后向参考帧,并根据POC进行参考帧排序。函数具体实现如下:

     
     
  1. static inline void x264_reference_build_list( x264_t *h, int i_poc )
  2. {
  3. int i;
  4. int b_ok;
  5. /* build ref list 0/1 */
  6. h->i_ref0 = 0; //前向参考帧索引
  7. h->i_ref1 = 0; //后向参考帧索引
  8. for( i = 0; h->frames.reference[i]; i++ )
  9. { //注意这里都是指针操作
  10. if( h->frames.reference[i]->i_poc < i_poc )
  11. { //小于当前帧POC的,放到前向参考帧列表中
  12. h->fref0[h->i_ref0++] = h->frames.reference[i];
  13. }
  14. else if( h->frames.reference[i]->i_poc > i_poc )
  15. { //大于当前帧POC的,放到后向参考帧列表中
  16. h->fref1[h->i_ref1++] = h->frames.reference[i];
  17. }
  18. }
  19. /* Order ref0 from higher to lower poc */
  20. do
  21. { /*采用冒泡排序(不知道使用dowhile+for循环与双重for循环有什么优势),对参考帧按照POC从高到低进行排序*/
  22. b_ok = 1;
  23. for( i = 0; i < h->i_ref0 - 1; i++ )
  24. {
  25. if( h->fref0[i]->i_poc < h->fref0[i+ 1]->i_poc )
  26. {
  27. XCHG( x264_frame_t*, h->fref0[i], h->fref0[i+ 1] );
  28. b_ok = 0;
  29. break;
  30. }
  31. }
  32. } while( !b_ok );
  33. /* Order ref1 from lower to higher poc (bubble sort) for B-frame */
  34. do
  35. {
  36. b_ok = 1;
  37. for( i = 0; i < h->i_ref1 - 1; i++ )
  38. {
  39. if( h->fref1[i]->i_poc > h->fref1[i+ 1]->i_poc )
  40. {
  41. XCHG( x264_frame_t*, h->fref1[i], h->fref1[i+ 1] );
  42. b_ok = 0;
  43. break;
  44. }
  45. }
  46. } while( !b_ok );
  47. /* In the standard, a P-frame's ref list is sorted by frame_num.
  48. * We use POC, but check whether explicit reordering is needed */
  49. h->b_ref_reorder[ 0] =
  50. h->b_ref_reorder[ 1] = 0;
  51. if( h->sh.i_type == SLICE_TYPE_P )
  52. {
  53. for( i = 0; i < h->i_ref0 - 1; i++ )
  54. if( h->fref0[i]->i_frame_num < h->fref0[i+ 1]->i_frame_num )
  55. {
  56. h->b_ref_reorder[ 0] = 1;
  57. break;
  58. }
  59. }
  60. h->i_ref1 = X264_MIN( h->i_ref1, h->frames.i_max_ref1 );
  61. h->i_ref0 = X264_MIN( h->i_ref0, h->frames.i_max_ref0 );
  62. h->i_ref0 = X264_MIN( h->i_ref0, h->param.i_frame_reference ); // if reconfig() has lowered the limit
  63. assert( h->i_ref0 + h->i_ref1 <= 16 );
  64. h->mb.pic.i_fref[ 0] = h->i_ref0; //为什么参考帧选择这两个,还没有搞懂
  65. h->mb.pic.i_fref[ 1] = h->i_ref1;
  66. }
4、初始化比特流,写入SPS以及PPS信息后就开始进行片级编码。

     
     
  1. if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers )
  2. { /*SPS和PPS是解码需要用到的信息,因此只有解码器解析了SPS和PPS信息
  3. 才能进行解码,这就是为什么在每个IDR帧前写入这些信息*/
  4. if( h->fenc->i_frame == 0 )
  5. { //仅仅在第一针写入sei信息
  6. /* identify ourself */
  7. x264_nal_start( h, NAL_SEI, NAL_PRIORITY_DISPOSABLE ); /*开始整理nal。*/
  8. x264_sei_version_write( h, &h->out.bs ); //写sei信息
  9. x264_nal_end( h );
  10. }
  11. /* generate sequence parameters */
  12. x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST ); /*开始整理nal。一个nal单元的首地址被赋值,
  13. 将要处理此新的nal单元;设置nal的优先权和类型。*/
  14. x264_sps_write( &h->out.bs, h->sps ); /*写SPS信息。
  15. 将序列参数集sps写进位流结构中h->out.bs
  16. 不是每次都要写SPS and PPS,只有碰见立即刷新片(NAL_SLICE_IDR)时才写*/
  17. x264_nal_end( h ); /*结束nal,整理nal
  18. (1)输出新nal单元的地址
  19. (2)自增表示下一个新nal单元的序号*/
  20. /* generate picture parameters */
  21. x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST ); /*开始整理nal。
  22. 一个nal单元的首地址被赋值,将要处理此新的nal单元;设置nal的优先权和类型。*/
  23. x264_pps_write( &h->out.bs, h->pps ); /*写PPS信息。
  24. 将序列参数集sps写进位流结构中h->out.bs
  25. 不是每次都要写SPS and PPS,只有碰见立即刷新片(NAL_SLICE_IDR)时才写*/
  26. x264_nal_end( h ); /*结束nal,整理nal
  27. (1)输出新nal单元的地址
  28. (2)自增表示下一个新nal单元的序号*/
  29. }
接着就开始片级编码x264_slices_write( h );

二、片级编码分析





未完!待续。。。








        首先来介绍下h265(HEVC)nal单元头,与h264的nal层相比,h265的nal unit header有两个字节构成,如下图所示:


从图中可以看出hHEVC的nal包结构与h264有明显的不同,hevc加入了nal所在的时间层的ID,取去除了nal_ref_idc,此信息合并到了naltype中,通常情况下F为0,layerid为0,TID为1。

扫描二维码关注公众号,回复: 2924185 查看本文章

        nal单元的类型有如下几种:

     


     
     
  1. enum NalUnitType
  2. {
  3. NAL_UNIT_CODED_SLICE_TRAIL_N = 0, // 0
  4. NAL_UNIT_CODED_SLICE_TRAIL_R, // 1
  5. NAL_UNIT_CODED_SLICE_TSA_N, // 2
  6. NAL_UNIT_CODED_SLICE_TLA, // 3 // Current name in the spec: TSA_R
  7. NAL_UNIT_CODED_SLICE_STSA_N, // 4
  8. NAL_UNIT_CODED_SLICE_STSA_R, // 5
  9. NAL_UNIT_CODED_SLICE_RADL_N, // 6
  10. NAL_UNIT_CODED_SLICE_DLP, // 7 // Current name in the spec: RADL_R
  11. NAL_UNIT_CODED_SLICE_RASL_N, // 8
  12. NAL_UNIT_CODED_SLICE_TFD, // 9 // Current name in the spec: RASL_R
  13. NAL_UNIT_RESERVED_10,
  14. NAL_UNIT_RESERVED_11,
  15. NAL_UNIT_RESERVED_12,
  16. NAL_UNIT_RESERVED_13,
  17. NAL_UNIT_RESERVED_14,
  18. NAL_UNIT_RESERVED_15, NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  19. NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  20. NAL_UNIT_CODED_SLICE_BLANT, // 17 // Current name in the spec: BLA_W_DLP
  21. NAL_UNIT_CODED_SLICE_BLA_N_LP, // 18
  22. NAL_UNIT_CODED_SLICE_IDR, // 19 // Current name in the spec: IDR_W_DLP
  23. NAL_UNIT_CODED_SLICE_IDR_N_LP, // 20
  24. NAL_UNIT_CODED_SLICE_CRA, // 21
  25. NAL_UNIT_RESERVED_22,
  26. NAL_UNIT_RESERVED_23,
  27. NAL_UNIT_RESERVED_24,
  28. NAL_UNIT_RESERVED_25,
  29. NAL_UNIT_RESERVED_26,
  30. NAL_UNIT_RESERVED_27,
  31. NAL_UNIT_RESERVED_28,
  32. NAL_UNIT_RESERVED_29,
  33. NAL_UNIT_RESERVED_30,
  34. NAL_UNIT_RESERVED_31,
  35. NAL_UNIT_VPS, // 32
  36. NAL_UNIT_SPS, // 33
  37. NAL_UNIT_PPS, // 34
  38. NAL_UNIT_ACCESS_UNIT_DELIMITER, // 35
  39. NAL_UNIT_EOS, // 36
  40. NAL_UNIT_EOB, // 37
  41. NAL_UNIT_FILLER_DATA, // 38
  42. NAL_UNIT_SEI, // 39 Prefix SEI
  43. NAL_UNIT_SEI_SUFFIX, // 40 Suffix SEI
  44. NAL_UNIT_RESERVED_41,
  45. NAL_UNIT_RESERVED_42,
  46. NAL_UNIT_RESERVED_43,
  47. NAL_UNIT_RESERVED_44,
  48. NAL_UNIT_RESERVED_45,
  49. NAL_UNIT_RESERVED_46,
  50. NAL_UNIT_RESERVED_47,
  51. NAL_UNIT_UNSPECIFIED_48,
  52. NAL_UNIT_UNSPECIFIED_49,
  53. NAL_UNIT_UNSPECIFIED_50,
  54. NAL_UNIT_UNSPECIFIED_51,
  55. NAL_UNIT_UNSPECIFIED_52,
  56. NAL_UNIT_UNSPECIFIED_53,
  57. NAL_UNIT_UNSPECIFIED_54,
  58. NAL_UNIT_UNSPECIFIED_55,
  59. NAL_UNIT_UNSPECIFIED_56,
  60. NAL_UNIT_UNSPECIFIED_57,
  61. NAL_UNIT_UNSPECIFIED_58,
  62. NAL_UNIT_UNSPECIFIED_59,
  63. NAL_UNIT_UNSPECIFIED_60,
  64. NAL_UNIT_UNSPECIFIED_61,
  65. NAL_UNIT_UNSPECIFIED_62,
  66. NAL_UNIT_UNSPECIFIED_63,
  67. NAL_UNIT_INVALID,
  68. };
下面接收下fu分组打包方式,fu分组包头格式如下:


fus包头包含了两个字节的payloadhdr,一个字节的fu header,fu header与h264一样,结构如下图,包含开始位(1b)、停止位(1b)、futype(6b)

paylodhdr两个自己的赋值,其实就是把hevc帧数据的nal unit header的naltype替换为49即可,下面是从ffmpeg源码中截取出来的fu打包方式代码片段:


     
     
  1. static void nal_send(AVFormatContext *ctx, const uint8_t *buf, int len, int last_packet_of_frame)
  2. {
  3. RTPMuxContext *rtp_ctx = ctx->priv_data;
  4. int rtp_payload_size = rtp_ctx->max_payload_size - RTP_HEVC_HEADERS_SIZE;
  5. int nal_type = (buf[ 0] >> 1) & 0x3F;
  6. /* send it as one single NAL unit? */
  7. if (len <= rtp_ctx->max_payload_size) //小于对定的最大值时,直接发送(最大值一般小于mtu)
  8. {
  9. /* use the original NAL unit buffer and transmit it as RTP payload */
  10. ff_rtp_send_data(ctx, buf, len, last_packet_of_frame);
  11. }
  12. else //大于最大值时进行fu分组发送
  13. {
  14. /*
  15. create the HEVC payload header and transmit the buffer as fragmentation units (FU)
  16. 0 1
  17. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  18. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  19. |F| Type | LayerId | TID |
  20. +-------------+-----------------+
  21. F = 0
  22. Type = 49 (fragmentation unit (FU))
  23. LayerId = 0
  24. TID = 1
  25. */
  26. rtp_ctx->buf[ 0] = 49 << 1;
  27. rtp_ctx->buf[ 1] = 1;
  28. //此处为paylaodhdr,规范赋值应该是替换hevc数据nal 的payloadhdr的type
  29. //rtp_ctx->buf[0] = (buf[0] &0x81) | (49<<1);
  30. //rtp_ctx->buf[1] = buf[1]
  31. /*
  32. create the FU header
  33. 0 1 2 3 4 5 6 7
  34. +-+-+-+-+-+-+-+-+
  35. |S|E| FuType |
  36. +---------------+
  37. S = variable
  38. E = variable
  39. FuType = NAL unit type
  40. */
  41. rtp_ctx->buf[ 2] = nal_type;
  42. /* set the S bit: mark as start fragment */
  43. rtp_ctx->buf[ 2] |= 1 << 7;
  44. /* pass the original NAL header */
  45. //此处要注意,当是分组的第一报数据时,应该覆盖掉前两个字节的数据,h264要覆盖前一个字节的数据,即是第一包要去除hevc帧数据的paylaodhdr
  46. buf += 2;
  47. len -= 2;
  48. while (len > rtp_payload_size)
  49. {
  50. /* complete and send current RTP packet */
  51. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, rtp_payload_size);
  52. ff_rtp_send_data(ctx, rtp_ctx->buf, rtp_ctx->max_payload_size, 0);
  53. buf += rtp_payload_size;
  54. len -= rtp_payload_size;
  55. /* reset the S bit */
  56. rtp_ctx->buf[ 2] &= ~( 1 << 7);
  57. }
  58. /* set the E bit: mark as last fragment */
  59. rtp_ctx->buf[ 2] |= 1 << 6;
  60. /* complete and send last RTP packet */
  61. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, len);
  62. ff_rtp_send_data(ctx, rtp_ctx->buf, len + 2, last_packet_of_frame);
  63. }
  64. }



通过rtp发送hevc视频数据,当hevc帧数据大于mtu时,应该进行fu分组发送,从上面代码流程就是对超过max_payload_size数据进行fu分组的流程,这个h264 fu-A很类似,很容易理解。


参考规范:

https://tools.ietf.org/html/draft-ietf-payload-rtp-h265-14

ffmpeg相关代码

https://www.ffmpeg.org/doxygen/2.5/rtpenc__hevc_8c_source.html



转载出处: https://blog.csdn.net/water1209/article/details/43706525

对H.264编码标准一直停留在理解原理的基础上,对于一个实际投入使用的编码器是如何构建起来一直感觉很神秘,于是决定在理解理论的基础上潜心于编码器实现框架。关于开源的H264编码器有很多,JMVC,T264、X264,这里选择X264,因为网上关于X264源码分析资源很多。X264编码器是一个开源的经过优化的高性能H.264编码器,目前最新的源码在本人的I5处理器的PC机上,编码1920x1080分辨率的视频序列在使用ultrafast配置的情况下,可以实现160fps左右的编码速度。

这里对源码分析并没有选择最新的源码,而是选用2009年2月份的版本,原因有二:一是这个版本是从网上下载下来的,已经是一个建立好的VS2008工程,对于像我这种用惯IDE调试的程序员来说,这可以大大提高源码阅读速度;二是虽然X264源码虽然几乎每天都有更新,但是从这个版本以后,最大的改动基本是针对多Slice编码的支持,其他都是在输入输出方面的一些改动,从这个版本学习可以较快进入编码部分的学习;三是这个版本当中已经有别人的很多的注释,便于自己理解;

一般将编码分为帧级、片级、宏块级编码,依次从上到下。下面就从这三个级分析X264编码流程:

一、帧级编码分析

定位到x264_encoder_encode这个函数,这个函数应该是H264编码最上层的函数,实现编码一帧视频。在进行下一步分析之前有必要了解,控制X264编码的全局性结构体x264_t,这个结构体控制着视频一帧一帧的编码,包括中间参考帧管理、码率控制、全局参数等一些重要参数和结构体。
下面是x264_t这个结构体的定义(这里仅对几个关键的结构和变量进行分析):


   
   
  1. struct x264_t
  2. {
  3. x264_param_t param; //编码器编码参数,包括量化步长、编码级别等等一些参数
  4. ………..
  5. int i_frame; //编码帧号,用于计算POC(picture of count 标识视频帧的解码顺序)
  6. ………..
  7. int i_nal_type; /* Nal 单元的类型,可以查看编码标准,有哪几种类型,需要理解的类型有:不分区(一帧作为一个片)非IDR图像的片;片分区A、片分区B、片分区C、IDR图像中的片、序列参数集、图像参数集 */
  8. int i_nal_ref_idc; /* Nal 单元的优先级别 取值范围[0,1,2,3],值越大表示优先级越高,此Nal单元就越重要 */
  9. /* We use only one SPS(序列参数集) and one PPS(图像参数集) */
  10. x264_sps_t sps_array[ 1]; //结构体的数组
  11. x264_sps_t *sps;
  12. x264_pps_t pps_array[ 1];
  13. x264_pps_t *pps;
  14. int i_idr_pic_id;
  15. ……
  16. struct
  17. {
  18. //这个结构体涉及到X264编码过程中的帧管理,理解这个结构体中的变量在编码标准的理论意义是非常重要的
  19. x264_frame_t *current[X264_BFRAME_MAX* 4+ 3]; /*已确定帧类型,待编码帧,每一个GOP在编码前,每一帧的类型在编码前已经确定。当进行编码时,从这里取出一帧数据。*/
  20. x264_frame_t *next[X264_BFRAME_MAX* 4+ 3]; //尚未确定帧类型的待编码帧,当确定后,会将此数组中的帧转移到current数组中去。
  21. x264_frame_t *unused[X264_BFRAME_MAX* 4 + X264_THREAD_MAX* 2 + 16+ 4]; /*这个数组用于回收那些在编码中分配的frame空间,当有新的需要时,直接拿过来用,不用重新分配新的空间,提高效率*/
  22. /* For adaptive B decision */
  23. x264_frame_t *last_nonb;
  24. /* frames used for reference + sentinels */
  25. x264_frame_t *reference[ 16+ 2]; //参考帧队列,注意参考帧都是重建帧
  26. int i_last_idr; /* 上一次刷新关键帧的帧号,配合前面的i_frame,可以用来计算POC */
  27. int i_input; /* Number of input frames already accepted */ //frames结构体中i_input指示当前输入的帧的(播放顺序)序号。
  28. int i_max_dpb; /* 分配解码图像缓冲的最大数量(DPB) */
  29. int i_max_ref0; //最大前向参考帧数量
  30. int i_max_ref1; //最大后向参考帧数量
  31. int i_delay; /* Number of frames buffered for B reordering */
  32. //i_delay设置为由B帧个数(线程个数)确定的帧缓冲延迟,在多线程情况下为i_delay = i_bframe + i_threads - 1。
  33. //而判断B帧缓冲填充是否足够则通过条件判断:h->frames.i_input <= h->frames.i_delay + 1 - h->param.i_threads。
  34. int b_have_lowres; /* Whether 1/2 resolution luma planes are being used */
  35. int b_have_sub8x8_esa;
  36. } frames; //指示和控制帧编码过程的结构
  37. /* current frame being encoded */
  38. x264_frame_t *fenc; //指向当前编码帧
  39. /* frame being reconstructed */
  40. x264_frame_t *fdec; //指向当前重建帧,重建帧的帧号要比当前编码帧的帧号小1
  41. /* references lists */
  42. int i_ref0; //前向参考帧的数量
  43. x264_frame_t *fref0[ 16+ 3]; /* 存放前向参考帧的数组(注意参考帧均是重建帧) */
  44. int i_ref1; //后向参考帧的数量
  45. x264_frame_t *fref1[ 16+ 3]; /* 存放后向参考帧的数组*/
  46. int b_ref_reorder[ 2];
  47. ……..
  48. };

定位到x264_encoder_encode这个函数,这个函数应该是H264编码最上层的函数,实现编码的帧级处理(如何进行参考帧管理、帧类型确定等等)。
 下面对x264_encoder_encode中几个关键函数以及关键部分进行分析:
1、x264_reference_update这个函数主要完成参考帧的更新,H.264的帧间预测需要使用参考帧,参考帧使用的都是已编码后的重建帧,每编码一帧的同时会重建此帧作为参考帧,在编码下一帧时,将此重建帧加入到参考帧队列中。函数实现如下:

   
   
  1. static inline void x264_reference_update( x264_t *h )
  2. {
  3. int i;
  4. if( h->fdec->i_frame >= 0 ) //重建帧帧数大于等于零时
  5. h->i_frame++; //当前编码帧的帧号要比重建帧的帧号大1
  6. if( !h->fdec->b_kept_as_ref ) /*如果重建帧不作为参考帧(不作为参考帧,当然不用加入参考帧队列了)*/
  7. { //when b frame is not used as reference frame
  8. if( h->param.i_threads > 1 )
  9. {
  10. x264_frame_push_unused( h, h->fdec );
  11. h->fdec = x264_frame_pop_unused( h );
  12. }
  13. return; //if b-frame is not used as reference, return
  14. }
  15. /* move lowres(低分辨率) copy of the image to the ref frame */
  16. for( i = 0; i < 4; i++)
  17. { /*暂时还不知道干嘛的*/
  18. XCHG( uint8_t*, h->fdec->lowres[i], h->fenc->lowres[i] );
  19. XCHG( uint8_t*, h->fdec->buffer_lowres[i], h->fenc->buffer_lowres[i] );
  20. }
  21. /* adaptive B decision needs a pointer, since it can't use the ref lists */
  22. if( h->sh.i_type != SLICE_TYPE_B )
  23. h->frames.last_nonb = h->fdec;
  24. /* move frame in the buffer */
  25. x264_frame_push( h->frames.reference, h->fdec ); /*把重建帧放入参考队列中*/
  26. if( h->frames.reference[h->frames.i_max_dpb] ) /*如果参考帧的个数大于解码图像缓存的最大数(decoded picture buffer(DPB))*/
  27. x264_frame_push_unused( h, x264_frame_shift( h->frames.reference ) ); /*取出参考队列中第一个参考重建帧,并放入暂时不用帧队列中*/
  28. h->fdec = x264_frame_pop_unused( h ); /*从暂时不用帧队列中,取出一帧作为新的重建帧buf*/
  29. }
2、帧排序部分,在H.264标准中采用编码顺序与显示顺序不同的编码方式,对于一个已经确定帧类型的待编码序列:IBBPBBP在编码时需要先排序为IPBBPBB,然后进行编码。在X264代码中,实现在如下部分:

   
   
  1. //确定帧的类型
  2. x264_stack_align( x264_slicetype_decide, h ); /*通过x264_slicetype_decide函数来确定决定h-frames.next[]中每一帧的类型*/
  3. /* 3: move some B-frames and 1 non-B to encode queue 这里来完成帧排序,还是有点巧妙的*/
  4. while( IS_X264_TYPE_B( h->frames.next[bframes]->i_type ) )
  5. bframes++; /*注意这个循环查找的作用,一方面可以确定第一个非B帧之前B帧的数量,也可以定位出第一个非B帧的位置*/
  6. x264_frame_push( h->frames.current, x264_frame_shift( &h->frames.next[bframes] ) ); /*取出第一个非B帧,并放到current指针第一个位置:通过这两步完成帧排序的(例如BBP->PBB)*/
  7. /* FIXME: when max B-frames > 3, BREF may no longer be centered after GOP closing */
  8. if( h->param.b_bframe_pyramid && bframes > 1 )
  9. {
  10. x264_frame_t *mid = x264_frame_shift( &h->frames.next[bframes/ 2] );
  11. mid->i_type = X264_TYPE_BREF;
  12. x264_frame_push( h->frames.current, mid );
  13. bframes--;
  14. }
  15. while( bframes-- )
  16. x264_frame_push( h->frames.current, x264_frame_shift( h->frames.next ) ); /*然后依次取出B帧,并放到current队列中*/
下面就可以从h->frames.current队列中取出第一帧放入h->fenc中
h->fenc = x264_frame_shift( h->frames.current );//从当前编码帧中取出第一帧,作为当前编码帧
   
   
然后就开始编码,首先当前编码帧(h->fenc)的类型,设定slice类型,这里就不解释了,关于IDR帧,执行了x264_reference_reset这个函数将参考帧队列清空。
接着进行相关参数的赋值,这里主要对POC的计算强调一下:
h->fenc->i_poc = 2 * (h->fenc->i_frame - h->frames.i_last_idr);//考虑到场编码,POC每帧增长为2,如果是场编码POC增长为1
   
   
3、重建参考帧列表(x264_reference_build_list),即将参考帧列表中的参考帧分为前向参考帧和后向参考帧,并根据POC进行参考帧排序。函数具体实现如下:

   
   
  1. static inline void x264_reference_build_list( x264_t *h, int i_poc )
  2. {
  3. int i;
  4. int b_ok;
  5. /* build ref list 0/1 */
  6. h->i_ref0 = 0; //前向参考帧索引
  7. h->i_ref1 = 0; //后向参考帧索引
  8. for( i = 0; h->frames.reference[i]; i++ )
  9. { //注意这里都是指针操作
  10. if( h->frames.reference[i]->i_poc < i_poc )
  11. { //小于当前帧POC的,放到前向参考帧列表中
  12. h->fref0[h->i_ref0++] = h->frames.reference[i];
  13. }
  14. else if( h->frames.reference[i]->i_poc > i_poc )
  15. { //大于当前帧POC的,放到后向参考帧列表中
  16. h->fref1[h->i_ref1++] = h->frames.reference[i];
  17. }
  18. }
  19. /* Order ref0 from higher to lower poc */
  20. do
  21. { /*采用冒泡排序(不知道使用dowhile+for循环与双重for循环有什么优势),对参考帧按照POC从高到低进行排序*/
  22. b_ok = 1;
  23. for( i = 0; i < h->i_ref0 - 1; i++ )
  24. {
  25. if( h->fref0[i]->i_poc < h->fref0[i+ 1]->i_poc )
  26. {
  27. XCHG( x264_frame_t*, h->fref0[i], h->fref0[i+ 1] );
  28. b_ok = 0;
  29. break;
  30. }
  31. }
  32. } while( !b_ok );
  33. /* Order ref1 from lower to higher poc (bubble sort) for B-frame */
  34. do
  35. {
  36. b_ok = 1;
  37. for( i = 0; i < h->i_ref1 - 1; i++ )
  38. {
  39. if( h->fref1[i]->i_poc > h->fref1[i+ 1]->i_poc )
  40. {
  41. XCHG( x264_frame_t*, h->fref1[i], h->fref1[i+ 1] );
  42. b_ok = 0;
  43. break;
  44. }
  45. }
  46. } while( !b_ok );
  47. /* In the standard, a P-frame's ref list is sorted by frame_num.
  48. * We use POC, but check whether explicit reordering is needed */
  49. h->b_ref_reorder[ 0] =
  50. h->b_ref_reorder[ 1] = 0;
  51. if( h->sh.i_type == SLICE_TYPE_P )
  52. {
  53. for( i = 0; i < h->i_ref0 - 1; i++ )
  54. if( h->fref0[i]->i_frame_num < h->fref0[i+ 1]->i_frame_num )
  55. {
  56. h->b_ref_reorder[ 0] = 1;
  57. break;
  58. }
  59. }
  60. h->i_ref1 = X264_MIN( h->i_ref1, h->frames.i_max_ref1 );
  61. h->i_ref0 = X264_MIN( h->i_ref0, h->frames.i_max_ref0 );
  62. h->i_ref0 = X264_MIN( h->i_ref0, h->param.i_frame_reference ); // if reconfig() has lowered the limit
  63. assert( h->i_ref0 + h->i_ref1 <= 16 );
  64. h->mb.pic.i_fref[ 0] = h->i_ref0; //为什么参考帧选择这两个,还没有搞懂
  65. h->mb.pic.i_fref[ 1] = h->i_ref1;
  66. }
4、初始化比特流,写入SPS以及PPS信息后就开始进行片级编码。

   
   
  1. if( i_nal_type == NAL_SLICE_IDR && h->param.b_repeat_headers )
  2. { /*SPS和PPS是解码需要用到的信息,因此只有解码器解析了SPS和PPS信息
  3. 才能进行解码,这就是为什么在每个IDR帧前写入这些信息*/
  4. if( h->fenc->i_frame == 0 )
  5. { //仅仅在第一针写入sei信息
  6. /* identify ourself */
  7. x264_nal_start( h, NAL_SEI, NAL_PRIORITY_DISPOSABLE ); /*开始整理nal。*/
  8. x264_sei_version_write( h, &h->out.bs ); //写sei信息
  9. x264_nal_end( h );
  10. }
  11. /* generate sequence parameters */
  12. x264_nal_start( h, NAL_SPS, NAL_PRIORITY_HIGHEST ); /*开始整理nal。一个nal单元的首地址被赋值,
  13. 将要处理此新的nal单元;设置nal的优先权和类型。*/
  14. x264_sps_write( &h->out.bs, h->sps ); /*写SPS信息。
  15. 将序列参数集sps写进位流结构中h->out.bs
  16. 不是每次都要写SPS and PPS,只有碰见立即刷新片(NAL_SLICE_IDR)时才写*/
  17. x264_nal_end( h ); /*结束nal,整理nal
  18. (1)输出新nal单元的地址
  19. (2)自增表示下一个新nal单元的序号*/
  20. /* generate picture parameters */
  21. x264_nal_start( h, NAL_PPS, NAL_PRIORITY_HIGHEST ); /*开始整理nal。
  22. 一个nal单元的首地址被赋值,将要处理此新的nal单元;设置nal的优先权和类型。*/
  23. x264_pps_write( &h->out.bs, h->pps ); /*写PPS信息。
  24. 将序列参数集sps写进位流结构中h->out.bs
  25. 不是每次都要写SPS and PPS,只有碰见立即刷新片(NAL_SLICE_IDR)时才写*/
  26. x264_nal_end( h ); /*结束nal,整理nal
  27. (1)输出新nal单元的地址
  28. (2)自增表示下一个新nal单元的序号*/
  29. }
接着就开始片级编码x264_slices_write( h );

二、片级编码分析





未完!待续。。。








        首先来介绍下h265(HEVC)nal单元头,与h264的nal层相比,h265的nal unit header有两个字节构成,如下图所示:


从图中可以看出hHEVC的nal包结构与h264有明显的不同,hevc加入了nal所在的时间层的ID,取去除了nal_ref_idc,此信息合并到了naltype中,通常情况下F为0,layerid为0,TID为1。

        nal单元的类型有如下几种:

     


   
   
  1. enum NalUnitType
  2. {
  3. NAL_UNIT_CODED_SLICE_TRAIL_N = 0, // 0
  4. NAL_UNIT_CODED_SLICE_TRAIL_R, // 1
  5. NAL_UNIT_CODED_SLICE_TSA_N, // 2
  6. NAL_UNIT_CODED_SLICE_TLA, // 3 // Current name in the spec: TSA_R
  7. NAL_UNIT_CODED_SLICE_STSA_N, // 4
  8. NAL_UNIT_CODED_SLICE_STSA_R, // 5
  9. NAL_UNIT_CODED_SLICE_RADL_N, // 6
  10. NAL_UNIT_CODED_SLICE_DLP, // 7 // Current name in the spec: RADL_R
  11. NAL_UNIT_CODED_SLICE_RASL_N, // 8
  12. NAL_UNIT_CODED_SLICE_TFD, // 9 // Current name in the spec: RASL_R
  13. NAL_UNIT_RESERVED_10,
  14. NAL_UNIT_RESERVED_11,
  15. NAL_UNIT_RESERVED_12,
  16. NAL_UNIT_RESERVED_13,
  17. NAL_UNIT_RESERVED_14,
  18. NAL_UNIT_RESERVED_15, NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  19. NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  20. NAL_UNIT_CODED_SLICE_BLANT, // 17 // Current name in the spec: BLA_W_DLP
  21. NAL_UNIT_CODED_SLICE_BLA_N_LP, // 18
  22. NAL_UNIT_CODED_SLICE_IDR, // 19 // Current name in the spec: IDR_W_DLP
  23. NAL_UNIT_CODED_SLICE_IDR_N_LP, // 20
  24. NAL_UNIT_CODED_SLICE_CRA, // 21
  25. NAL_UNIT_RESERVED_22,
  26. NAL_UNIT_RESERVED_23,
  27. NAL_UNIT_RESERVED_24,
  28. NAL_UNIT_RESERVED_25,
  29. NAL_UNIT_RESERVED_26,
  30. NAL_UNIT_RESERVED_27,
  31. NAL_UNIT_RESERVED_28,
  32. NAL_UNIT_RESERVED_29,
  33. NAL_UNIT_RESERVED_30,
  34. NAL_UNIT_RESERVED_31,
  35. NAL_UNIT_VPS, // 32
  36. NAL_UNIT_SPS, // 33
  37. NAL_UNIT_PPS, // 34
  38. NAL_UNIT_ACCESS_UNIT_DELIMITER, // 35
  39. NAL_UNIT_EOS, // 36
  40. NAL_UNIT_EOB, // 37
  41. NAL_UNIT_FILLER_DATA, // 38
  42. NAL_UNIT_SEI, // 39 Prefix SEI
  43. NAL_UNIT_SEI_SUFFIX, // 40 Suffix SEI
  44. NAL_UNIT_RESERVED_41,
  45. NAL_UNIT_RESERVED_42,
  46. NAL_UNIT_RESERVED_43,
  47. NAL_UNIT_RESERVED_44,
  48. NAL_UNIT_RESERVED_45,
  49. NAL_UNIT_RESERVED_46,
  50. NAL_UNIT_RESERVED_47,
  51. NAL_UNIT_UNSPECIFIED_48,
  52. NAL_UNIT_UNSPECIFIED_49,
  53. NAL_UNIT_UNSPECIFIED_50,
  54. NAL_UNIT_UNSPECIFIED_51,
  55. NAL_UNIT_UNSPECIFIED_52,
  56. NAL_UNIT_UNSPECIFIED_53,
  57. NAL_UNIT_UNSPECIFIED_54,
  58. NAL_UNIT_UNSPECIFIED_55,
  59. NAL_UNIT_UNSPECIFIED_56,
  60. NAL_UNIT_UNSPECIFIED_57,
  61. NAL_UNIT_UNSPECIFIED_58,
  62. NAL_UNIT_UNSPECIFIED_59,
  63. NAL_UNIT_UNSPECIFIED_60,
  64. NAL_UNIT_UNSPECIFIED_61,
  65. NAL_UNIT_UNSPECIFIED_62,
  66. NAL_UNIT_UNSPECIFIED_63,
  67. NAL_UNIT_INVALID,
  68. };
下面接收下fu分组打包方式,fu分组包头格式如下:


fus包头包含了两个字节的payloadhdr,一个字节的fu header,fu header与h264一样,结构如下图,包含开始位(1b)、停止位(1b)、futype(6b)

paylodhdr两个自己的赋值,其实就是把hevc帧数据的nal unit header的naltype替换为49即可,下面是从ffmpeg源码中截取出来的fu打包方式代码片段:


   
   
  1. static void nal_send(AVFormatContext *ctx, const uint8_t *buf, int len, int last_packet_of_frame)
  2. {
  3. RTPMuxContext *rtp_ctx = ctx->priv_data;
  4. int rtp_payload_size = rtp_ctx->max_payload_size - RTP_HEVC_HEADERS_SIZE;
  5. int nal_type = (buf[ 0] >> 1) & 0x3F;
  6. /* send it as one single NAL unit? */
  7. if (len <= rtp_ctx->max_payload_size) //小于对定的最大值时,直接发送(最大值一般小于mtu)
  8. {
  9. /* use the original NAL unit buffer and transmit it as RTP payload */
  10. ff_rtp_send_data(ctx, buf, len, last_packet_of_frame);
  11. }
  12. else //大于最大值时进行fu分组发送
  13. {
  14. /*
  15. create the HEVC payload header and transmit the buffer as fragmentation units (FU)
  16. 0 1
  17. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  18. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  19. |F| Type | LayerId | TID |
  20. +-------------+-----------------+
  21. F = 0
  22. Type = 49 (fragmentation unit (FU))
  23. LayerId = 0
  24. TID = 1
  25. */
  26. rtp_ctx->buf[ 0] = 49 << 1;
  27. rtp_ctx->buf[ 1] = 1;
  28. //此处为paylaodhdr,规范赋值应该是替换hevc数据nal 的payloadhdr的type
  29. //rtp_ctx->buf[0] = (buf[0] &0x81) | (49<<1);
  30. //rtp_ctx->buf[1] = buf[1]
  31. /*
  32. create the FU header
  33. 0 1 2 3 4 5 6 7
  34. +-+-+-+-+-+-+-+-+
  35. |S|E| FuType |
  36. +---------------+
  37. S = variable
  38. E = variable
  39. FuType = NAL unit type
  40. */
  41. rtp_ctx->buf[ 2] = nal_type;
  42. /* set the S bit: mark as start fragment */
  43. rtp_ctx->buf[ 2] |= 1 << 7;
  44. /* pass the original NAL header */
  45. //此处要注意,当是分组的第一报数据时,应该覆盖掉前两个字节的数据,h264要覆盖前一个字节的数据,即是第一包要去除hevc帧数据的paylaodhdr
  46. buf += 2;
  47. len -= 2;
  48. while (len > rtp_payload_size)
  49. {
  50. /* complete and send current RTP packet */
  51. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, rtp_payload_size);
  52. ff_rtp_send_data(ctx, rtp_ctx->buf, rtp_ctx->max_payload_size, 0);
  53. buf += rtp_payload_size;
  54. len -= rtp_payload_size;
  55. /* reset the S bit */
  56. rtp_ctx->buf[ 2] &= ~( 1 << 7);
  57. }
  58. /* set the E bit: mark as last fragment */
  59. rtp_ctx->buf[ 2] |= 1 << 6;
  60. /* complete and send last RTP packet */
  61. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, len);
  62. ff_rtp_send_data(ctx, rtp_ctx->buf, len + 2, last_packet_of_frame);
  63. }
  64. }



通过rtp发送hevc视频数据,当hevc帧数据大于mtu时,应该进行fu分组发送,从上面代码流程就是对超过max_payload_size数据进行fu分组的流程,这个h264 fu-A很类似,很容易理解。


参考规范:

https://tools.ietf.org/html/draft-ietf-payload-rtp-h265-14

ffmpeg相关代码

https://www.ffmpeg.org/doxygen/2.5/rtpenc__hevc_8c_source.html



转载出处: https://blog.csdn.net/water1209/article/details/43706525

        首先来介绍下h265(HEVC)nal单元头,与h264的nal层相比,h265的nal unit header有两个字节构成,如下图所示:


从图中可以看出hHEVC的nal包结构与h264有明显的不同,hevc加入了nal所在的时间层的ID,取去除了nal_ref_idc,此信息合并到了naltype中,通常情况下F为0,layerid为0,TID为1。

        nal单元的类型有如下几种:

     


  
  
  1. enum NalUnitType
  2. {
  3. NAL_UNIT_CODED_SLICE_TRAIL_N = 0, // 0
  4. NAL_UNIT_CODED_SLICE_TRAIL_R, // 1
  5. NAL_UNIT_CODED_SLICE_TSA_N, // 2
  6. NAL_UNIT_CODED_SLICE_TLA, // 3 // Current name in the spec: TSA_R
  7. NAL_UNIT_CODED_SLICE_STSA_N, // 4
  8. NAL_UNIT_CODED_SLICE_STSA_R, // 5
  9. NAL_UNIT_CODED_SLICE_RADL_N, // 6
  10. NAL_UNIT_CODED_SLICE_DLP, // 7 // Current name in the spec: RADL_R
  11. NAL_UNIT_CODED_SLICE_RASL_N, // 8
  12. NAL_UNIT_CODED_SLICE_TFD, // 9 // Current name in the spec: RASL_R
  13. NAL_UNIT_RESERVED_10,
  14. NAL_UNIT_RESERVED_11,
  15. NAL_UNIT_RESERVED_12,
  16. NAL_UNIT_RESERVED_13,
  17. NAL_UNIT_RESERVED_14,
  18. NAL_UNIT_RESERVED_15, NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  19. NAL_UNIT_CODED_SLICE_BLA, // 16 // Current name in the spec: BLA_W_LP
  20. NAL_UNIT_CODED_SLICE_BLANT, // 17 // Current name in the spec: BLA_W_DLP
  21. NAL_UNIT_CODED_SLICE_BLA_N_LP, // 18
  22. NAL_UNIT_CODED_SLICE_IDR, // 19 // Current name in the spec: IDR_W_DLP
  23. NAL_UNIT_CODED_SLICE_IDR_N_LP, // 20
  24. NAL_UNIT_CODED_SLICE_CRA, // 21
  25. NAL_UNIT_RESERVED_22,
  26. NAL_UNIT_RESERVED_23,
  27. NAL_UNIT_RESERVED_24,
  28. NAL_UNIT_RESERVED_25,
  29. NAL_UNIT_RESERVED_26,
  30. NAL_UNIT_RESERVED_27,
  31. NAL_UNIT_RESERVED_28,
  32. NAL_UNIT_RESERVED_29,
  33. NAL_UNIT_RESERVED_30,
  34. NAL_UNIT_RESERVED_31,
  35. NAL_UNIT_VPS, // 32
  36. NAL_UNIT_SPS, // 33
  37. NAL_UNIT_PPS, // 34
  38. NAL_UNIT_ACCESS_UNIT_DELIMITER, // 35
  39. NAL_UNIT_EOS, // 36
  40. NAL_UNIT_EOB, // 37
  41. NAL_UNIT_FILLER_DATA, // 38
  42. NAL_UNIT_SEI, // 39 Prefix SEI
  43. NAL_UNIT_SEI_SUFFIX, // 40 Suffix SEI
  44. NAL_UNIT_RESERVED_41,
  45. NAL_UNIT_RESERVED_42,
  46. NAL_UNIT_RESERVED_43,
  47. NAL_UNIT_RESERVED_44,
  48. NAL_UNIT_RESERVED_45,
  49. NAL_UNIT_RESERVED_46,
  50. NAL_UNIT_RESERVED_47,
  51. NAL_UNIT_UNSPECIFIED_48,
  52. NAL_UNIT_UNSPECIFIED_49,
  53. NAL_UNIT_UNSPECIFIED_50,
  54. NAL_UNIT_UNSPECIFIED_51,
  55. NAL_UNIT_UNSPECIFIED_52,
  56. NAL_UNIT_UNSPECIFIED_53,
  57. NAL_UNIT_UNSPECIFIED_54,
  58. NAL_UNIT_UNSPECIFIED_55,
  59. NAL_UNIT_UNSPECIFIED_56,
  60. NAL_UNIT_UNSPECIFIED_57,
  61. NAL_UNIT_UNSPECIFIED_58,
  62. NAL_UNIT_UNSPECIFIED_59,
  63. NAL_UNIT_UNSPECIFIED_60,
  64. NAL_UNIT_UNSPECIFIED_61,
  65. NAL_UNIT_UNSPECIFIED_62,
  66. NAL_UNIT_UNSPECIFIED_63,
  67. NAL_UNIT_INVALID,
  68. };
下面接收下fu分组打包方式,fu分组包头格式如下:


fus包头包含了两个字节的payloadhdr,一个字节的fu header,fu header与h264一样,结构如下图,包含开始位(1b)、停止位(1b)、futype(6b)

paylodhdr两个自己的赋值,其实就是把hevc帧数据的nal unit header的naltype替换为49即可,下面是从ffmpeg源码中截取出来的fu打包方式代码片段:


  
  
  1. static void nal_send(AVFormatContext *ctx, const uint8_t *buf, int len, int last_packet_of_frame)
  2. {
  3. RTPMuxContext *rtp_ctx = ctx->priv_data;
  4. int rtp_payload_size = rtp_ctx->max_payload_size - RTP_HEVC_HEADERS_SIZE;
  5. int nal_type = (buf[ 0] >> 1) & 0x3F;
  6. /* send it as one single NAL unit? */
  7. if (len <= rtp_ctx->max_payload_size) //小于对定的最大值时,直接发送(最大值一般小于mtu)
  8. {
  9. /* use the original NAL unit buffer and transmit it as RTP payload */
  10. ff_rtp_send_data(ctx, buf, len, last_packet_of_frame);
  11. }
  12. else //大于最大值时进行fu分组发送
  13. {
  14. /*
  15. create the HEVC payload header and transmit the buffer as fragmentation units (FU)
  16. 0 1
  17. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  18. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  19. |F| Type | LayerId | TID |
  20. +-------------+-----------------+
  21. F = 0
  22. Type = 49 (fragmentation unit (FU))
  23. LayerId = 0
  24. TID = 1
  25. */
  26. rtp_ctx->buf[ 0] = 49 << 1;
  27. rtp_ctx->buf[ 1] = 1;
  28. //此处为paylaodhdr,规范赋值应该是替换hevc数据nal 的payloadhdr的type
  29. //rtp_ctx->buf[0] = (buf[0] &0x81) | (49<<1);
  30. //rtp_ctx->buf[1] = buf[1]
  31. /*
  32. create the FU header
  33. 0 1 2 3 4 5 6 7
  34. +-+-+-+-+-+-+-+-+
  35. |S|E| FuType |
  36. +---------------+
  37. S = variable
  38. E = variable
  39. FuType = NAL unit type
  40. */
  41. rtp_ctx->buf[ 2] = nal_type;
  42. /* set the S bit: mark as start fragment */
  43. rtp_ctx->buf[ 2] |= 1 << 7;
  44. /* pass the original NAL header */
  45. //此处要注意,当是分组的第一报数据时,应该覆盖掉前两个字节的数据,h264要覆盖前一个字节的数据,即是第一包要去除hevc帧数据的paylaodhdr
  46. buf += 2;
  47. len -= 2;
  48. while (len > rtp_payload_size)
  49. {
  50. /* complete and send current RTP packet */
  51. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, rtp_payload_size);
  52. ff_rtp_send_data(ctx, rtp_ctx->buf, rtp_ctx->max_payload_size, 0);
  53. buf += rtp_payload_size;
  54. len -= rtp_payload_size;
  55. /* reset the S bit */
  56. rtp_ctx->buf[ 2] &= ~( 1 << 7);
  57. }
  58. /* set the E bit: mark as last fragment */
  59. rtp_ctx->buf[ 2] |= 1 << 6;
  60. /* complete and send last RTP packet */
  61. memcpy(&rtp_ctx->buf[RTP_HEVC_HEADERS_SIZE], buf, len);
  62. ff_rtp_send_data(ctx, rtp_ctx->buf, len + 2, last_packet_of_frame);
  63. }
  64. }



通过rtp发送hevc视频数据,当hevc帧数据大于mtu时,应该进行fu分组发送,从上面代码流程就是对超过max_payload_size数据进行fu分组的流程,这个h264 fu-A很类似,很容易理解。


参考规范:

https://tools.ietf.org/html/draft-ietf-payload-rtp-h265-14

ffmpeg相关代码

https://www.ffmpeg.org/doxygen/2.5/rtpenc__hevc_8c_source.html



转载出处: https://blog.csdn.net/water1209/article/details/43706525

猜你喜欢

转载自blog.csdn.net/yanceyxin/article/details/81979733