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](https://i-blog.csdnimg.cn/blog_migrate/e04bcfe66faeed6da2e54cff5c71b685.png)
在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](https://i-blog.csdnimg.cn/blog_migrate/e025ec74e93c0fdf31380f18a7f19555.png)
优先级 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的长度
。
运行结果: