Android音视频开发——SPS分析与提取

前言

H.264码流中的NALU进行了一个简单的划分,标出了NALU的类型和长度等信息。因为我们在解析SPS和PPS中要使用到指数哥伦布编码的解析,所以有必要了解一下指数哥伦布编码。

哥伦布编码

指数哥伦布码正常来说,可以拓展位k阶,但是在H264中使用的是0阶指数哥伦布编码,在H.264中使用ue(v)表示0阶无符号指数哥伦布编码的解码过程,用se(v)表示0阶有符号指数哥伦布编码过程

无符号指数哥伦布编码

用来表示无符号整数k阶指数哥伦布编码的生成步骤如下:

(1)将数字以二进制形式写出,去掉最低位的k个比特位,之后加1

(2)计算留下的比特数位数,将此数减1,即是需要增加的前导0的个数

(3)将第一步中去掉的最低个比特位补回到比特串尾部

0阶无符号指数哥伦布编码过程

0阶无符号指数哥伦布编码最后生成的比特串格式为"前缀1后缀",前缀和后缀的长度是相同的。

假如我们待编码的数字codeNum = 4,0阶无符号指数哥伦布编码的步骤如下:

(1)将数字以二进制写出,4的二进制为100,因为0阶指数哥伦布编码所有,所以不用去掉低位

(2)将上面的二进制+1,100加1为101,留下的比特数为3,3-1=2,所以需要增加前导0的个数为2

(3)因为第一步没有去掉,所有这一步不进行任何操作,最终生成的比特串为00101

下面对不同codeNum进行编码结果

codeNum codeNum+1 codeNum+1的二进制 需补前缀0的个数 编码后的比特串(红色表示补的前缀0)
0 1 1 0 1
1 2 10 1(0) 010
2 3 11 1(0) 011
3 4 100 2(00) 00100
4 5 101 2(00) 00101
5 6 110 2(00) 00110
6 7 111 2(00) 00111

0阶无符号指数哥伦布编码的解析过程如下

(1)找到第一个不为0的bit,并记录总共找到了0的个数(num),这个时候读到的这个bit肯定是1

(2)然后读num个后缀

(3)1后缀转变成十进制就是原来的codeNum,codeNum = (1 <<i) + 后缀(十进制) - 1;

比如比特串的二进制为:00101,首先找到第一个不为0的比特,前面0的个数为2,然后再读2个后缀10,10十进制为2,这个时候codeNum = (1 << 2) + 2 - 1 = 4 + 3 - 1 = 5

代码实现

  • 1.将编码好的数据5还原成原来的数据4
    (1)5的二进制是101,首先我们需要将5转成字节码8位0000 0101
   //5的字节码是101,转成8位字节码
        byte data = 6 & 0xFF;

(2)我们需要统计0的个数,也就是首先要获取第一个不是0的个数

               000 00101 
&              000 10000
=              000 00000

当前元素&0x80右移动N位就可以获取当前的元素,当获取到第一个元素是非0跳出

        //0000 0101
        int i = 3;
        //统计0的个数
        int zeroNum = 0;
        while (i < 8) {
            //找到第一个不为0
            if ((data & (0x80 >> i)) != 0) {
                break;
            }
            zeroNum++;
            i++;
        }

(3)获取到0的个数后,跳过获取到第一个非0的元素,也就是i++.找到第一个不为0之后,往后找zeroNum个数的字节
比如6的字节码是111,找到第一个非0之后往后找两个字节,也就是11,字节码的值是2*1+1,也就是说,无论多少字节,最后一个非0的数据一定是+1,而最后的前面的数据分边左移1位

        i++;
        //找到第一个不为0之后,往后找zeroNum个数
        int value = 0;
        for (int j = 0; j < zeroNum; j++) {
            value <<= 1;
            //找到元素不为0 的个数
            if ((data & (0x80 >> i)) != 0) {
                value += 1;
            }
            i++;
        }
        int result=(1<<zeroNum)+value-1;
        System.out.println(result);

SPS解析分析

我们来看一个h264的sps信息


image.png

NALU

  • NALU:H264编码数据存储或传输的基本单元,一般H264码流最开始的两个NALU是SPS和PPS,第三个NALU是IDR。SPS、PPS、SEI这三种NALU不属于帧的范畴。
    NALU语法结构.png
  • 原始的NALU单元组成
    [start code] + [NALU header] + [NALU payload];
  • H.264码流在网络中传输时实际是以NALU的形式进行传输的.
    • 每个NALU由一个字节的HeaderRBSP组成.
    • NAL Header 的组成为:
      forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)

SPS解析

1.1 NAL Header
网上的.png

帧类型.png

我们可以再x264.h源码中看到这行代码


image.png
image.png
1.2 profile_idc: 编码等级,有以下取值
Options:
66 Baseline(直播)
77 Main(一般场景)
88 Extended
100 High (FRExt)
110 High 10 (FRExt)
122 High 4:2:2 (FRExt)
144 High 4:4:4 (FRExt)

我们上面的64是十六进制,转成二进制是100,也就是说我们现在的编码等级是High。等级越高,代表视频越清晰

1.3 level_idc : 标识当前码流的等级
Options:
10 1 (supports only QCIF format and below with 380160 samples/sec)
11 1.1 (CIF and below. 768000 samples/sec)
12 1.2 (CIF and below. 1536000 samples/sec)
13 1.3 (CIF and below. 3041280 samples/sec)
20 2 (CIF and below. 3041280 samples/sec)
21 2.1 (Supports HHR formats. Enables Interlace support. 5 068 800 samples/sec)
22 2.2 (Supports SD/4CIF formats. Enables Interlace support. 5184000 samples/sec)
30 3 (Supports SD/4CIF formats. Enables Interlace support. 10368000 samples/sec)
31 3.1 (Supports 720p HD format. Enables Interlace support. 27648000 samples/sec)
32 3.2 (Supports SXGA format. Enables Interlace support. 55296000 samples/sec)
40 4 (Supports 2Kx1K format. Enables Interlace support. 62914560 samples/sec)
41 4.1 (Supports 2Kx1K format. Enables Interlace support. 62914560 samples/sec)
42 4.2 (Supports 2Kx1K format. Frame coding only. 125829120 samples/sec)
50 5 (Supports 3672x1536 format. Frame coding only. 150994944 samples/sec)
51 5.1 (Supports 4096x2304 format. Frame coding only. 251658240 samples/sec)
1.4 chroma_format_idc 与亮度取样对应的色度取样

chroma_format_idc 的值应该在 0到 3的范围内(包括 0和 3)。当 chroma_format_idc不存在时,应推断其值为 1(4:2:0的色度格式)。

chroma_format_idc 色彩格式
0 单色
1 4:2:0
2 4:2:2
3 4:4:4
1.5 seq_parameter_set_id:哥伦布编码
image.png
  • seq_parameter_set_id值应该是从0到31,包括0和31
  • 当可用的情况下,编码器应该在sps值不同的情况下使用不同的seq_parameter_set_id值,而不是变化某一特定值的
1.6 bit_depth_luma_minus8 表示视频位深
  • 0 High 只支持8bit
  • 1 High10 才支持10bit


    image.png
1.7 log2_max_frame_num_minus4

这个句法元素主要是为读取另一个句法元素 frame_num 服务的,frame_num 是最重要的句法元素之一,它标识所属图像的解码顺序。可以在句法表看到, fram-num的解码函数是 ue(v),函数中的 v 在这里指定:

v = log2_max_frame_num_minus4 + 4
从另一个角度看,这个句法元素同时也指明了 frame_num 的所能达到的最大值:
MaxFrameNum = 2( log2_max_frame_num_minus4 + 4 )

变量 MaxFrameNum 表示 frame_num 的最大值,在后文中可以看到,在解码过程中它也是一个非常重要的变量。

值得注意的是 frame_num 是循环计数的,即当它到达 MaxFrameNum 后又从 0 重新开始新一轮的计数。 解码器必须要有机制检测这种循环, 不然会引起类似千年虫的问题,在图像的顺序上造成混乱。

1.8 pic_order_cnt_type
  • 指明了picture order count的编码方法,picture order count标识图像的播放顺序。
1.9 log2_max_pic_order_cnt_lsb_minus4
  • 指明了变量 MaxPicOrderCntLsb 的值:
  • MaxPicOrderCntLsb = 2( log2_max_pic_order_cnt_lsb_minus4 + 4 )
  • 该变量在 pic_order_cnt_type = 0 时使用。
image.png

SPS分析与提取代码

   //哥伦布编码
    public static int columbusCode(byte[] pBuff) {
        //统计0的个数
        int zeroNum = 0;
        while (i < pBuff.length * 8) {
            //找到第一个不为0
            if ((pBuff[i / 8] & (0x80 >> (i % 8))) != 0) {
                break;
            }
            zeroNum++;
            i++;
        }
        i++;
        //找到第一个不为0之后,往后找zeroNum个数
        int value = 0;
        for (int j = 0; j < zeroNum; j++) {
            value <<= 1;
            //找到元素不为0 的个数
            if ((pBuff[i / 8] & (0x80 >> (i % 8))) != 0) {
                value += 1;
            }
            i++;
        }
        int result = (1 << zeroNum) + value - 1;
        return result;
    }

    public static byte[] hexStringToByteArray(String s) {
        //十六进制转byte数组
        int len = s.length();
        byte[] bs = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            bs[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
        }
        return bs;
    }


    static int i = 0;

    /**
     * @bitIndex 字节数位数
     */
    private static int paraseH264(int bitIndex, byte[] h264) {
        int value = 0;
        for (int j = 0; j < bitIndex; j++) {
            value <<= 1;
            //获取到每个字节
            if ((h264[i / 8] & (0x80 >> (i % 8))) != 0) {
                value += 1;
            }
            i++;
        }
        return value;
    }

    public static void main(String[] args) {
        //十六进制转byte数组
        byte[] h264 = hexStringToByteArray("00 00 00 01 67 64 00 15 AC D9 41 70 C6 84 00 00 03 00 04 00 00 03 00 F0 3C 58 B6 58".replace(" ", ""));
        i = 4 * 8;
        //禁止位
        int forbidden_zero_bit = paraseH264(1, h264);
        System.out.println("forbidden_zero_bit:" + forbidden_zero_bit);
        //重要性
        int nal_ref_idc = paraseH264(2, h264);
        System.out.println("nal_ref_idc:" + nal_ref_idc);
        //帧类型
        int nal_unit_type = paraseH264(5, h264);
        System.out.println("nal_unit_type:" + nal_unit_type);
        if (nal_unit_type == 7) {
            //编码等级
            int profile_idc = paraseH264(8, h264);
            System.out.println("profile_idc:" + profile_idc);
            //64后面的(00),基本用不到
            //当constrained_set0_flag值为1的时候,就说明码流应该遵循基线profile(Baseline profile)的所有约束.constrained_set0_flag值为0时,说明码流不一定要遵循基线profile的所有约束。
            int constraint_set0_flag = paraseH264(1, h264);//(h264[1] & 0x80)>>7;
            // 当constrained_set1_flag值为1的时候,就说明码流应该遵循主profile(Main profile)的所有约束.constrained_set1_flag值为0时,说明码流不一定要遵
            int constraint_set1_flag = paraseH264(1, h264);//(h264[1] & 0x40)>>6;
            //当constrained_set2_flag值为1的时候,就说明码流应该遵循扩展profile(Extended profile)的所有约束.constrained_set2_flag值为0时,说明码流不一定要遵循扩展profile的所有约束。
            int constraint_set2_flag = paraseH264(1, h264);//(h264[1] & 0x20)>>5;
            //注意:当constraint_set0_flag,constraint_set1_flag或constraint_set2_flag中不只一个值为1的话,那么码流必须满足所有相应指明的profile约束。
            int constraint_set3_flag = paraseH264(1, h264);//(h264[1] & 0x10)>>4;
            // 4个零位
            int reserved_zero_4bits = paraseH264(4, h264);

            //码流等级
            int level_idc = paraseH264(8, h264);
            System.out.println("level_idc:" + level_idc);
            //(AC)就是哥伦布编码
            int seq_parameter_set_id = columbusCode(h264);
            System.out.println("seq_parameter_set_id:" + seq_parameter_set_id);
            if (profile_idc == 100) {
                int chroma_format_idc = columbusCode(h264);
                System.out.println("chroma_format_idc:" + chroma_format_idc);
                int bit_depth_luma_minus8 = columbusCode(h264);
                System.out.println("bit_depth_luma_minus8:" + bit_depth_luma_minus8);
                int bit_depth_chroma_minus8 = columbusCode(h264);
                System.out.println("bit_depth_chroma_minus8:" + bit_depth_chroma_minus8);
                int qpprime_y_zero_transform_bypass_flag = paraseH264(1, h264);
                System.out.println("qpprime_y_zero_transform_bypass_flag:" + qpprime_y_zero_transform_bypass_flag);
                //缩放标志位
                int seq_scaling_matrix_present_flag = paraseH264(1, h264);
                System.out.println("seq_scaling_matrix_present_flag:" + seq_scaling_matrix_present_flag);
            }
            //最大帧率
            int log2_max_frame_num_minus4 = columbusCode(h264);
            System.out.println("log2_max_frame_num_minus4:" + log2_max_frame_num_minus4);
            //确定播放顺序和解码顺序的映射
            int pic_order_cnt_type = columbusCode(h264);
            System.out.println("pic_order_cnt_type:" + pic_order_cnt_type);
            int log2_max_pic_order_cnt_lsb_minus4 = columbusCode(h264);
            System.out.println("log2_max_pic_order_cnt_lsb_minus4:" + log2_max_pic_order_cnt_lsb_minus4);
            int num_ref_frames = columbusCode(h264);
            System.out.println("num_ref_frames:" + num_ref_frames);
            int gaps_in_frame_num_value_allowed_flag = paraseH264(1, h264);
            System.out.println("gaps_in_frame_num_value_allowed_flag:" + gaps_in_frame_num_value_allowed_flag);
            System.out.println("------startBit " + i);//83
            int pic_width_in_mbs_minus1 = columbusCode(h264);
            System.out.println("------startBit " + i);//92
            int pic_height_in_map_units_minus1 = columbusCode(h264);
            int width = (pic_width_in_mbs_minus1 + 1) * 16;
            int height = (pic_height_in_map_units_minus1 + 1) * 16;
            System.out.println("width :  " + width + "   height: " + height);
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容