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;
}