音视频数据处理入门:解析h264视频码流

1、获取h264视频码流

从mp4中抽取视频码流:ffmpeg -i 001.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 001.h264

-codec copy: 从mp4中拷贝
-bsf: h264_mp4toannexb: 从mp4拷贝到annexB封装
-f h264: 采用h264格式
// 视频不要太长,建议用-t 10选项只剪切一部分出来解析。

2、h264码流结构

H.264码流由一个个NAL(网络抽象层)单元组成:

image-20210428222244412

H.264/AVC视频编码标准中,整个系统框架被分成两个层面:视频编码层VCL和网络抽象层NAL。

其中VCL负责有效表示视频数据的内容,NAL负责格式化数据并提供头信息,以保证数据适合在各种信道和存储介质上的传输。

①NALU包含一个字节的头信息和一系列来自VCL的称为原始字节序列载荷的字节流
每个NALU之间通过startcode(起始码)分隔;如果NALU对应的片段是一帧的开始,则startcode为0x00 00 00 01,否则为0x00 00 01。

NALU头信息(1字节)forbidden_bit(恒为0),priority(优先级)2bit,type(类型)5bit

image-20210427202745670

优先级 00:表示没有参考作用,可丢弃,如B slice、SEI等;

优先级非零:表示该NALU不可丢弃,如SPS、PPS、I Slice、P Slice等。


3、解析h264码流

因为NAL的语法中没有给出长度信息,所以在实际传输和存储中,我们要自定义额外的结构来管理各个NAL单元的定界。

AVI和TS容器采取的是字节流的语法格式,在NALU之前增加0x00 00 00 01的同步码;而MP4容器中没有同步码,使用一个长度数组来表示NALU的长度。

我们要直接解析H.264裸流,需要:
a. 先从码流中找到起始码,分离出NALU;
b. 然后再分析NALU头信息。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum {
    NALU_TYPE_SLICE = 1,    // 不分区,非IDR图像
    NALU_TYPE_DPA,          // 片分区A
    NALU_TYPE_DPB,          // 片分区B
    NALU_TYPE_DPC,          // 片分区C
    NALU_TYPE_IDR,          // IDR图像中的片
    NALU_TYPE_SEI,         // 补充增强信息单元
    NALU_TYPE_SPS,          // 序列参数集
    NALU_TYPE_PPS,          // 图像参数集
    NALU_TYPE_AUD,          // 分界符
    NALU_TYPE_EOSEQ,        // 序列结束符
    NALU_TYPE_EOSTREAM,     // 码流结束符
    NALU_TYPE_FILL          // 填充
} NaluType;

typedef enum {
    NALU_PRIORITY_DISPOSABLE = 0,
    NALU_PRIORITY_LOW,
    NALU_PRIORITY_HIGH,
    NALU_PRIORITY_HIGHEST
} NaluPriority;


typedef struct {
    int pos;        // NALU对应起始码,在文件中的位置
    int codelen;    // 起始码大小
    int header;     // (0) + (priority)2bits + (types)5bits
} Nalu_t;

void fill_typestr(char *type_str, int type) {
    switch(type) {
        case NALU_TYPE_SLICE:sprintf(type_str,"SLICE");break;
        case NALU_TYPE_DPA:sprintf(type_str,"DPA");break;
        case NALU_TYPE_DPB:sprintf(type_str,"DPB");break;
        case NALU_TYPE_DPC:sprintf(type_str,"DPC");break;
        case NALU_TYPE_IDR:sprintf(type_str,"IDR");break;
        case NALU_TYPE_SEI:sprintf(type_str,"SEI");break;
        case NALU_TYPE_SPS:sprintf(type_str,"SPS");break;
        case NALU_TYPE_PPS:sprintf(type_str,"PPS");break;
        case NALU_TYPE_AUD:sprintf(type_str,"AUD");break;
        case NALU_TYPE_EOSEQ:sprintf(type_str,"EOSEQ");break;
        case NALU_TYPE_EOSTREAM:sprintf(type_str,"EOSTREAM");break;
        case NALU_TYPE_FILL:sprintf(type_str,"FILL");break;
    }
}

void fill_priostr(char *priostr, int prio) {
    switch(prio) {
        case NALU_PRIORITY_DISPOSABLE:sprintf(priostr,"DISPOS");break;
        case NALU_PRIORITY_LOW:sprintf(priostr,"LOW");break;
        case NALU_PRIORITY_HIGH:sprintf(priostr,"HIGH");break;
        case NALU_PRIORITY_HIGHEST:sprintf(priostr,"HIGHEST");break;
    }
}


int startcode_len(unsigned char *buf) {
    if (buf[0] == 0 && buf[1] == 0) {
        if (buf[2] == 1) return 3;
        else if(buf[3] == 1) return 4;
    }    
    return -1;
}

void h264_analyse(const char *file) {
    FILE *fp = fopen(file, "r");
    unsigned char *buf = malloc(30000000); // 根据视频大小设
    Nalu_t *nalu = malloc(sizeof(*nalu));
    memset(nalu, 0, sizeof(Nalu_t));
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    int filesize = ftell(fp);
    rewind(fp);

    // 先读部分数据,解析第一个NALU
    fread(buf, 1, 100, fp);
    nalu->pos = 0;
    nalu->codelen = startcode_len(&buf[nalu->pos]);

    printf("| NUM | CODE |   PRIO |   TYPE  | LEN |\n");

    int cnt = 0;                // nalu个数
    int left = 0, right = 100;  // left表示当前搜索位置,right表示当前文件偏移
    int found, len;             
    while (1) {
        int headidx = nalu->pos + nalu->codelen + 1;
        nalu->header = buf[headidx];

        int type = nalu->header & 0x1F;
        char type_str[10];
        fill_typestr(type_str, type);

        int prio = (nalu->header & 0x60)>>5;
        char prio_str[10];
        fill_priostr(prio_str, prio);
        
        // 找到下一个起始码
        found = 0; len = 0;
        left += nalu->codelen + 1;  // 跳过startcode和header
        while (!found) {
            if (left < right) {
                if (left + 4 >= filesize) // 防止在函数内数组越界
                    goto clean;
                len = startcode_len(&buf[left++]);
                if (len > 0) found = 1;
            } else {
                if (right + 100 < filesize) {
                    fread(&buf[right], 1, 100, fp);
                    right += 100;
                } else {    // 剩余的数据不到100B字节
                    fread(&buf[right], 1, filesize - right, fp);
                    right = filesize;
                }
            }
        }
        int nalulen = left - nalu->pos - nalu->codelen;  // NALU大小,不包括起始码

        printf("|%5d|%6d|%8s|%9s|%5d|\n", cnt, nalu->codelen, prio_str, type_str, nalulen);
        cnt++;
        nalu->codelen = len;
        nalu->pos = left - 1;
    }

clean:
    free(nalu);
    free(buf);
    fclose(fp);
}

int main(int argc, char const* argv[])
{
    h264_analyse("test.h264"); 
    return 0;
}

重点:Nalu_t结构和{left,right}双指针的设计。建议自己动手画一下起始码、nalu头、视频流的分布,思考如何去分隔不同单元,特别是如何计算nalu的长度

运行结果:
image-20210428233729519

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值