H264在网络传输的是NALU,NALU的结构是:NAL头+RBSP,
其中NAL头占一个字节,其低5个bit位表示NAL type。
RBSP 为原始字节序列载荷
在实际的H264数据帧中,往往帧NAL type前面带有00 00 00 01 或 00 00 01分隔符,
一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧,然后是P帧
视频传输原理
视频是利用人眼视觉暂留的原理,通过播放一系列的图片,使人眼产生运动的感觉。单纯传输视频画面,视频量非常大,
对现有的网络和存储来说是不可接受的。为了能够使视频便于传输和存储,人们发现视频有大量重复的信息,如果将重
复信息在发送端去掉,在接收端恢复出来,这样就大大减少了视频数据的文件,因此有了H.264视频压缩标准。
在H.264压缩标准中I帧、P帧、B帧用于表示传输的视频画面。
1、I帧
I帧又称帧内编码帧,是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频
序列中的第一个帧始终都是I帧,因为它是关键帧。
2、P帧
P帧又称帧间预测编码帧,需要参考前面的I帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别。
解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。与I帧相比,P帧通常占用更少的数据位,但不足是,由于P帧对
前面的P和I参考帧有着复杂的依耐性,因此对传输错误非常敏感。
3、B帧
B帧又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,
通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是对解码性能要求较高。
总结:
I帧只需考虑本帧;P帧记录的是与前一帧的差别;B帧记录的是前一帧及后一帧的差别,能节约更多的空间,视频文件小了,但相对来说解码的时候
就比较麻烦。因为在解码时,不仅要用之前缓存的画面,而且要知道下一个I或者P的画面,对于不支持B帧解码的播放器容易卡顿。
视频监控系统中预览的视频画面是实时的,对画面的流畅性要求较高。采用I帧、P帧进行视频传输可以提高网络的适应能力,且能降低解码成本。所以现阶段的视频解码都只采用I帧和P帧进行传输。海康摄像机编码,I帧间隔是50,含49个P帧。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define TAB44 " "
#define PRINTF_DEBUG
#define PRTNTF_STR_LEN 10
/************************************************************************************************************
** nalu header: 负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,
覆盖了所有片级以上的语法级别(NALU的作用, 方便网络传输)
**
-------------------------------------------------------------------------------------------------------------
** 字段名称 | 长度(bits) | 有关描述
-------------------------------------------------------------------------------------------------------------
** forbidden_bit | 1 | 编码中默认值为0, 当网络识别此单元中存在比特错误时, 可将其设为1, 以便接收方丢掉该单元
** nal_reference_idc | 2 | 0~3标识这个NALU的重要级别
** nal_unit_type | 5 | NALU的类型(类型1~12是H.264定义的, 类型24~31是用于H.264以外的,
RTP负荷规范使用这其中的一些值来定义包聚合和分裂, 其他值为H.264保留)
** nal_unit_type:
0 未使用
1 未使用Data Partitioning, 非IDR图像的Slice
2 使用Data Partitioning且为Slice A
3 使用Data Partitioning且为Slice B
4 使用Data Partitioning且为Slice C
5 IDR图像的Slice(立即刷新)
6 补充增强信息(SEI)
7 序列参数集(sequence parameter set, SPS)
8 图像参数集(picture parameter set, PPS)
9 分界符
10 序列结束
11 码流结束
12 填充
13...23 保留
24...31 未使用
** SPS, PPS. SLICE等信息就不解析了. 为了减少bits, 用了哥伦布编码(自己解析比较麻烦, 但是网上有很多).
** SPS信息说明:
1. 视频宽高, 帧率等信息;
2. seq_parameter_set_id, 指明本序列参数集的id号, 这个id号将被picture参数集引用;
3. pic_width_in_mbs_minus1, 加1指定以宏块(16*16)为单位的每个解码图像的宽度, 即width = (pic_width_in_mbs_minus1 + 1) * 16
4. pic_height_in_map_units_minus1;
5. pic_order_cnt_type, 视频的播放顺序序号叫做POC(picture order count), 取值0,1,2;
6. time_scale, fixed_frame_rate_flag, 计算帧率(fps).
视频帧率信息在SPS的VUI parameters syntax中, 需要根据time_scale, fixed_frame_rate_flag计算得到: fps = time_scale / num_units_in_tick.
但是需要判断参数timing_info_present_flag是否存在, 若不存在表示FPS在信息流中无法获取.
同时还存在另外一种情况: fixed_frame_rate_flag为1时, 两个连续图像的HDR输出时间频率为单位, 获取的fps是实际的2倍.
** PPS信息说明:
1. pic_parameter_set_id, 用以指定本参数集的序号, 该序号在各片的片头被引用;
2. seq_parameter_set_id, 指明本图像参数集所引用的序列参数集的序号;
3. 其他高深的暂时还不理解, 指明参考帧队列等.
** SLICE信息说明:
1. slice_type, 片的类型;
2. pic_parameter_set_id, 引用的图像索引;
3. frame_num, 每个参考帧都有一个连续的frame_num作为它们的标识, 它指明了各图像的解码顺序. 非参考帧也有,但没有意义;
4. least significant bits;
5. 综合三种poc(pic_order_cnt_type), 类型2应该是最省bit的, 因为直接从frame_num获得, 但是序列方式限制最大;
类型1, 只需要一定的bit量在sps标志出一些信息还在slice header中表示poc的变化, 但是比类型0要节省bit, 但是其序列并不是随意的, 要周期变化;
对于类型0因为要对poc的lsb(pic_order_cnt_lsb, last bit)进行编码所以用到的bit最多, 优点是序列可以随意.
** 自我理解, 不一定准确(这边算显示顺序, 要根据SPS中的pic_order_cnt_type, 为2, 意味着码流中没有B帧, frame_num即为显示顺序;
为1, 依赖frame_num求解POC; 为0, 把POC的低位编进码流内, 但这只是低位, 而POC的高位PicOrderCntMsb则要求解码器自行计数,
计数方式依赖于前一编码帧(PrevPicOrderCntMsb与PrevPicOrderCntLsb.
** 一般的码流分析所见(未仔细证实): pic_order_cnt_type=2, 只有frame_num(无B帧);
pic_order_cnt_type=1, 暂未分析到;
pic_order_cnt_type=0, pic_order_cnt_lsb指示显示顺序, 一般为偶数增长(0, 2, 4, 6, 据说是什么场方式和帧方式, 场时其实是0 0 2 2 4 4).
** 编码与显示的原因: 视频编码顺序与视频的播放顺序, 并不完全相同, 视频编码时, 如果采用了B帧编码, 由于B帧很多时候都是双向预测得来的,
这时会先编码B帧的后向预测图像(P帧), 然后再进行B帧编码, 因此会把视频原来的播放顺序打乱, 以新的编码顺序输出码流,
而在解码断接收到码流后, 需要把顺序还原成原本的播放顺序, 以输出正确的视频. 在编解码中, 视频的播放顺序序号叫做POC(picture order count).
** 总结: 1. 码流中有很多SPS(序列), 一个序列中有多个图像, 一个图像中有多个片, 一个片中有多个块;
2. SPS中有seq_parameter_set_id. PPS中有pic_parameter_set_id, 并通过seq_parameter_set_id指明关联的序列.
SLICE中有pic_parameter_set_id, 指明关联的图像;
3. SPS中可计算宽高以及帧率, pic_order_cnt_type(显示顺序的类型);
SLICE HEADER中可算出解码的顺序, 以及根据pic_order_cnt_type算出显示顺序.
************************************************************************************************************/
typedef enum e_h264_nalu_priority
{
NALU_PRIORITY_DISPOSABLE = 0,
NALU_PRIORITY_LOW = 1,
NALU_PRIORITY_HIGH = 2,
NALU_PRIORITY_HIGHEST = 3,
} E_H264_NALU_PRIORITY;
typedef enum e_h264_nalu_type
{
NALU_TYPE_SLICE = 1,
NALU_TYPE_DPA = 2,
NALU_TYPE_DPB = 3,
NALU_TYPE_DPC = 4,
NALU_TYPE_IDR = 5,
NALU_TYPE_SEI = 6,
NALU_TYPE_SPS = 7,
NALU_TYPE_PPS = 8,
NALU_TYPE_AUD = 9,
NALU_TYPE_EOSEQ = 10,
NALU_TYPE_EOSTREAM = 11,
NALU_TYPE_FILL = 12,
} E_H264_NALU_TYPE;
typedef struct t_h264_nalu_header
{
unsigned char forbidden_bit:1, nal_reference_idc:2, nal_unit_type:5;
} T_H264_NALU_HEADER;
typedef struct t_h264_nalu
{
int startCodeLen;
T_H264_NALU_HEADER h264NaluHeader;
unsigned int bodyLen;
unsigned char *bodyData;
} T_H264_NALU;
/**********************************************************************************
1. h264的起始码: 0x000001(3 Bytes)或0x00000001(4 Bytes);
2. 文件流中用起始码来区分NALU.
***********************************************************************************/
static int FindStartCode3Bytes(unsigned char *scData)
{
int isFind = 0;
if ((0==scData[0]) && (0==scData[1]) && (1==scData[2]))
{
isFind = 1;
}
return isFind;
}
static int FindStartCode4Bytes(unsigned char *scData)
{
int isFind = 0;
if ((0==scData[0]) && (0==scData[1]) && (0==scData[2]) && (1 == scData[3]))
{
isFind = 1;
}
return isFind;
}
static int GetNaluDataLen(int startPos, int h264BitsSize, unsigned char *h264Bits)
{
int parsePos = 0;
parsePos = startPos;
while (parsePos < h264BitsSize)
{
if (FindStartCode3Bytes(&h264Bits[parsePos]))
{
return parsePos - startPos;
}
else if (FindStartCode4Bytes(&h264Bits[parsePos]))
{
return parsePos - startPos;
}
else
{
parsePos++;
}
}
return parsePos - startPos; // if file is end
}
static void ParseNaluData(const unsigned int naluLen, unsigned char* const nuluData)
{
static int naluNum = 0;
unsigned char *data = NULL;
unsigned char priorityStr[PRTNTF_STR_LEN+1] = {0};
unsigned char typeStr[PRTNTF_STR_LEN+1] = {0};
T_H264_NALU_HEADER h264NaluHeader = {0};
data = nuluData;
memset(&h264NaluHeader, 0x0, sizeof(T_H264_NALU_HEADER));
h264NaluHeader.nal_reference_idc = data[0]>>5 & 0x3;
h264NaluHeader.nal_unit_type = data[0] & 0x1f;
naluNum++;
#ifdef PRINTF_DEBUG
switch (h264NaluHeader.nal_reference_idc)
{
case NALU_PRIORITY_DISPOSABLE:
sprintf(priorityStr, "DISPOS");
break;
case NALU_PRIORITY_LOW:
sprintf(priorityStr, "LOW");
break;
case NALU_PRIORITY_HIGH:
sprintf(priorityStr, "HIGH");
break;
case NALU_PRIORITY_HIGHEST:
sprintf(priorityStr, "HIGHEST");
break;
default:
break;
}
switch (h264NaluHeader.nal_unit_type)
{
case NALU_TYPE_SLICE:
sprintf(typeStr, "SLICE");
break;
case NALU_TYPE_DPA:
sprintf(typeStr, "DPA");
break;
case NALU_TYPE_DPB:
sprintf(typeStr, "DPB");
break;
case NALU_TYPE_DPC:
sprintf(typeStr, "DPC");
break;
case NALU_TYPE_IDR:
sprintf(typeStr, "IDR");
break;
case NALU_TYPE_SEI:
sprintf(typeStr, "SEI");
break;
case NALU_TYPE_SPS:
sprintf(typeStr, "SPS");
break;
case NALU_TYPE_PPS:
sprintf(typeStr, "PPS");
break;
case NALU_TYPE_AUD:
sprintf(typeStr, "AUD");
break;
case NALU_TYPE_EOSEQ:
sprintf(typeStr, "EOSEQ");
break;
case NALU_TYPE_EOSTREAM:
sprintf(typeStr, "EOSTREAM");
break;
case NALU_TYPE_FILL:
sprintf(typeStr, "FILL");
break;
default:
break;
}
printf("%5d| %7s| %6s| %8d|\n", naluNum, priorityStr, typeStr, naluLen);
#endif
}
int main(int argc, char *argv[])
{
int fileLen = 0;
int naluLen = 0;
int h264BitsPos = 0;
unsigned char *h264Bits = NULL;
unsigned char *naluData = NULL;
FILE *fp = NULL;
if (2 != argc)
{
printf("Usage: flvparse **.flv\n");
return -1;
}
fp = fopen(argv[1], "rb");
if (!fp)
{
printf("open file[%s] error!\n", argv[1]);
return -1;
}
fseek(fp, 0, SEEK_END);
fileLen = ftell(fp);
fseek(fp, 0, SEEK_SET);
h264Bits = (unsigned char*)malloc(fileLen);
if (!h264Bits)
{
printf("maybe file is too long, or memery is not enough!\n");
fclose(fp);
return -1;
}
memset(h264Bits, 0x0, fileLen);
if (fread(h264Bits, 1, fileLen, fp) < 0)
{
printf("read file data to h264Bits error!\n");
fclose(fp);
free(h264Bits);
h264Bits = NULL;
return -1;
}
fclose(fp);
printf("-----+-------- NALU Table ------+\n");
printf(" NUM | IDC | TYPE | LEN |\n");
printf("-----+--------+-------+---------+\n");
while (h264BitsPos < (fileLen-4))
{
if (FindStartCode3Bytes(&h264Bits[h264BitsPos]))
{
naluLen = GetNaluDataLen(h264BitsPos+3, fileLen, h264Bits);
naluData = (unsigned char*)malloc(naluLen);
if (naluData)
{
memset(naluData, 0x0, naluLen);
memcpy(naluData, h264Bits+h264BitsPos+3, naluLen);
ParseNaluData(naluLen, naluData);
free(naluData);
naluData = NULL;
}
h264BitsPos += (naluLen+3);
}
else if (FindStartCode4Bytes(&h264Bits[h264BitsPos]))
{
naluLen = GetNaluDataLen(h264BitsPos+4, fileLen, h264Bits);
naluData = (unsigned char*)malloc(naluLen);
if (naluData)
{
memset(naluData, 0x0, naluLen);
memcpy(naluData, h264Bits+h264BitsPos+4, naluLen);
ParseNaluData(naluLen, naluData);
free(naluData);
naluData = NULL;
}
h264BitsPos += (naluLen+4);
}
else
{
h264BitsPos++;
}
}
return 0;
}