一些基本概念
YUV/RGB處理以及PCM處理處于對視音頻原始數據的處理,但是視音頻在傳輸的過程中是以視音頻碼流傳輸的。接下來介紹一下視頻碼流:
H.264的標準定義如下:同時也是MPEG-4第十部分,是由ITU-T視頻編碼專家組(VCEG)和ISO/IEC動態圖像專家組(MPEG)聯合組成的聯合視頻組(JVT,Joint Video Team)提出的高度壓縮數字視頻編解碼器標準。
什么是H.264
先通過PR編輯軟件的導出列表形象生動地感受一下H.264格式:
H.264原始碼流(又稱為“裸流”)是由一個一個的NALU組成的,結構是這樣的:
每個NALU之間通過起始碼進行分隔,起始碼分成兩種:0x000001(3Byte)或者0x00000001(4Byte)。NALU對應的Slice為一幀的開始就用0x00000001,否則就用0x000001。H.264碼流解析的步驟就是首先從碼流中搜索0x000001和0x00000001,分離出NALU;然后再分析NALU的各個字段 。
什么是I幀、P幀、B幀
視頻壓縮中,每幀代表一幅靜止的圖像。而在實際壓縮時,會采取各種算法減少數據的容量,而IPB就是最常見的。I幀是關鍵幀(幀內壓縮),而P是向前搜索、B是雙向搜索,都是基于I幀來壓縮數據。
I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀數據就可以完成(因為包含完整畫面)
P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面數據,只有與前一幀的畫面差別的數據)
B幀是雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別(具體比較復雜,有4種情況),換言之,要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累~
網絡上的電影很多采用了B幀,因為B幀記錄的是前后幀的差別,比P幀能節約更多的空間。但是如果遇到不支持B幀的播放器就會很尷尬,因為雖然文件通過B幀壓縮確實縮小了非常多,但是在解碼時,不僅要用之前緩存的畫面,還要知道下一個I或者P的畫面。如果B幀丟失,就會用之前的畫面簡單重復,就會造成畫面卡,B幀越多,畫面越卡。
一般來說,I 幀的壓縮率是7(跟JPG差不多),P 幀是20,B 幀可以達到50。
在如上圖中,GOP (Group of Pictures)長度為13,S0~S7 表示 8個視點,T0~T12 為 GOP的 13個時刻。每個 GOP包含幀數為視點數 GOP 長度的乘積。在該圖中一個 GOP 中,包含94 個 B幀。B 幀占一個 GOP 總幀數的 90.38%。GOP 越長,B 幀所占比例更高,編碼的率失真性能越高
解釋一下GOP,是圖像組的意思,表示編碼的視頻序列分成了一組一組有序的幀的集合進行編碼。
H.264編碼原理
H.264采用的編碼算法就是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
視頻中一般會存在一段變化不大的圖像畫面,我們可以先編碼出一個完整的圖像幀t1
,之后的圖像幀t2
就可以不編碼全部圖像,而只寫入與t1
幀的差別,之后的圖像幀參考t3
幀仍舊可以參照這種方式繼續進行編碼。這樣循環下去就可以得到一個序列。如果下一個圖像幀t4
與之前的圖像變化很大,無法參考前面的圖像幀,就結束上一序列,并開始下一序列。
序列就可以定義為一段圖像編碼后的數據流,以I幀開始,到下一個I幀結束,H.264中圖像就是以序列為單位進行組織的。
H.264壓縮方法
知道視頻是如何編碼的了,那么怎樣具體進行編碼呢?有以下步驟:
(1)分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多。
(2)定義幀:將每組內各幀圖像定義為三種類型,即 I 幀、B 幀和 P 幀;
(3)預測幀:以I幀做為基礎幀,以 I 幀預測 P 幀,再由 I 幀和 P 幀預測 B 幀;
(4)數據傳輸:最后將 I 幀數據與預測的差值信息進行存儲和傳輸。
幀內(Intraframe)壓縮也稱為空間壓縮(Spatial compression)。當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。幀內一般采用有損壓縮算法,由于幀內壓縮是編碼一個完整的圖像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼 jpeg 差不多。
幀間(Interframe)壓縮的原理是:相鄰幾幀的數據有很大的相關性,或者說前后兩幀信息變化很小的特點。也即連續的視頻其相鄰幀之間具有冗余信息,根據這一特性,壓縮相鄰幀之間的冗余量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的數據進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
IDR幀
重點看一下P幀的定義:P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼.......
,P幀是可以參照前面的P幀的,并且GOP雖然一定是I幀開始的,但在一個GOP中不一定只有2個I幀,可能包含幾個I幀,只有第一個I幀才是關鍵幀。但是在這種情況下,舉個例子:I P B P B P B B B P1 I1 P2 B
,為了方便表示,將需要用到的圖像幀定義為P1 I1 P2;P2幀除了會參照I1幀之外,還會參考P1幀,如果P1與I幀畫面相差較大的話,那么不但沒有意義還會造成畫面問題,所以我們需要一種幀能夠禁止后面的幀向前面的幀參照,這就是IDR幀。
I 和 IDR 幀都是使用幀內預測的。它們都是同一個東西而已,在編碼和解碼中為了方便,要首個 I 幀和其他 I 幀區別開,所以才把第一個首個 I 幀叫 IDR,這樣就方便控制編碼和解碼流程。IDR 幀的作用是立刻刷新,使錯誤不致傳播,從IDR幀開始,重新算一個新的序列開始編碼。而 I 幀不具有隨機訪問的能力,這個功能是由 IDR 承擔。IDR 會導致DPB(參考幀列表——這是關鍵所在)清空,而 I 不會。IDR 圖像一定是 I 圖像,但I圖像不一定是 IDR 圖像。一個序列中可以有很多的I圖像,I 圖像之后的圖像可以引用 I 圖像之間的圖像做運動參考。一個序列中可以有很多的 I 圖像,I 圖像之后的圖象可以引用I圖像之間的圖像做運動參考。
詳解H.264 NALU語法結構
首先NALU是H.264編碼數據存儲或傳輸的基本單元,一般H.264碼流最開始的兩個NALU是SPS和PPS,第三個NALU是IDR。而SPS、PPS、SEI這三種NALU不屬于幀的范疇,它們的定義如下:
SPS(Sequence Parameter Sets):序列參數集,作用于一系列連續的編碼圖像。
PPS(Picture Parameter Set):圖像參數集,作用于編碼視頻序列中一個或多個獨立的圖像。
SEI(Supplemental enhancement information):附加增強信息,包含了視頻畫面定時等信息,一般放在主編碼圖像數據之前,在某些應用中,它可以被省略掉。
IDR(Instantaneous Decoding Refresh):即時解碼刷新。
HRD(Hypothetical Reference Decoder):假想碼流調度器。
語法結構
在H.264/AVC視頻編碼標準中,整個系統框架被分為了兩個層面:視頻編碼層面(VCL)和網絡抽象層面(NAL)。其中,前者負責有效表示視頻數據的內容,而后者則負責格式化數據并提供頭信息,以保證數據適合各種信道和存儲介質上的傳輸。因此我們平時的每幀數據就是一個NAL單元(SPS與PPS除外)。在實際的H264數據幀中,往往幀前面帶有00 00 00 01 或 00 00 01分隔符,一般來說編碼器編出的首幀數據為PPS與SPS,接著為I幀……
一個H.264文件如下:
在H264碼流中,都是以"0x00 0x00 0x01"或者"0x00 0x00 0x00 0x01"為開始碼的,找到開始碼之后,使用開始碼之后的第一個字節的低 5 位判斷是否為 7(sps)或者 8(pps), 及 data[4] & 0x1f == 7 || data[4] & 0x1f == 8。然后對獲取的 nal 去掉開始碼之后進行 base64 編碼,得到的信息就可以用于 sdp。 sps和pps需要用逗號分隔開來。
上圖中,00 00 00 01是一個NALU的起始標志。后面的第一個字節,0x67,是NALU的類型,type &0x1f==0x7表示這個NALU是SPS,type &0x1f==0x8表示是PPS。
H.264基本碼流結構分兩層,視頻編碼層VCL和網絡適配層NAL,這樣使信號處理和網絡傳輸分離,而H.264碼流在網絡中傳輸時實際上是以一系列的NALU組成,原始的NALU單元組成:
[start code] + [NALU header] + [NALU payload]
[start code]占1字節,為0x000001或0x00000001。
其中[NALU header]如下:
forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)
00 00 00 01 為起始符,67 即 nal_unit_type。
0x67的二進制是 0110 0111
則 forbidden_zero_bit(1bit) = 0;
nal_ref_idc(2bit) = 3;
nal_unit_type(5bit) = 7;即 SPS 類型。
更多關于H.264NALU的語法可以看這幾篇:
https://blog.csdn.net/wutong_login/article/details/5818763
https://blog.csdn.net/wutong_login/article/details/5824509
應用:分離NALU并分析字段
來自雷神:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum {
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,
} NaluType;
typedef enum {
NALU_PRIORITY_DISPOSABLE = 0,
NALU_PRIRITY_LOW = 1,
NALU_PRIORITY_HIGH = 2,
NALU_PRIORITY_HIGHEST = 3
} NaluPriority;
typedef struct
{
int startcodeprefix_len; //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
unsigned len; //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
unsigned max_size; //! Nal Unit Buffer size
int forbidden_bit; //! should be always FALSE
int nal_reference_idc; //! NALU_PRIORITY_xxxx
int nal_unit_type; //! NALU_TYPE_xxxx
char *buf; //! contains the first byte followed by the EBSP
} NALU_t;
FILE *h264bitstream = NULL; //!< the bit stream file
int info2=0, info3=0;
static int FindStartCode2 (unsigned char *Buf){
if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=1) return 0; //0x000001?
else return 1;
}
static int FindStartCode3 (unsigned char *Buf){
if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=0 || Buf[3] !=1) return 0;//0x00000001?
else return 1;
}
int GetAnnexbNALU (NALU_t *nalu){
int pos = 0;
int StartCodeFound, rewind;
unsigned char *Buf;
if ((Buf = (unsigned char*)calloc (nalu->max_size , sizeof(char))) == NULL)
printf ("GetAnnexbNALU: Could not allocate Buf memory\n");
nalu->startcodeprefix_len=3;
if (3 != fread (Buf, 1, 3, h264bitstream)){
free(Buf);
return 0;
}
info2 = FindStartCode2 (Buf);
if(info2 != 1) {
if(1 != fread(Buf+3, 1, 1, h264bitstream)){
free(Buf);
return 0;
}
info3 = FindStartCode3 (Buf);
if (info3 != 1){
free(Buf);
return -1;
}
else {
pos = 4;
nalu->startcodeprefix_len = 4;
}
}
else{
nalu->startcodeprefix_len = 3;
pos = 3;
}
StartCodeFound = 0;
info2 = 0;
info3 = 0;
while (!StartCodeFound){
if (feof (h264bitstream)){
nalu->len = (pos-1)-nalu->startcodeprefix_len;
memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);
nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
free(Buf);
return pos-1;
}
Buf[pos++] = fgetc (h264bitstream);
info3 = FindStartCode3(&Buf[pos-4]);
if(info3 != 1)
info2 = FindStartCode2(&Buf[pos-3]);
StartCodeFound = (info2 == 1 || info3 == 1);
}
// Here, we have found another start code (and read length of startcode bytes more than we should
// have. Hence, go back in the file
rewind = (info3 == 1)? -4 : -3;
if (0 != fseek (h264bitstream, rewind, SEEK_CUR)){
free(Buf);
printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
}
// Here the Start code, the complete NALU, and the next start code is in the Buf.
// The size of Buf is pos, pos+rewind are the number of bytes excluding the next
// start code, and (pos+rewind)-startcodeprefix_len is the size of the NALU excluding the start code
nalu->len = (pos+rewind)-nalu->startcodeprefix_len;
memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);//
nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
free(Buf);
return (pos+rewind);
}
/**
* Analysis H.264 Bitstream
* @param url Location of input H.264 bitstream file.
*/
int simplest_h264_parser(char *url){
NALU_t *n;
int buffersize=100000;
//FILE *myout=fopen("output_log.txt","wb+");
FILE *myout=stdout;
h264bitstream=fopen(url, "rb+");
if (h264bitstream==NULL){
printf("Open file error\n");
return 0;
}
n = (NALU_t*)calloc (1, sizeof (NALU_t));
if (n == NULL){
printf("Alloc NALU Error\n");
return 0;
}
n->max_size=buffersize;
n->buf = (char*)calloc (buffersize, sizeof (char));
if (n->buf == NULL){
free (n);
printf ("AllocNALU: n->buf");
return 0;
}
int data_offset=0;
int nal_num=0;
printf("-----+-------- NALU Table ------+---------+\n");
printf(" NUM | POS | IDC | TYPE | LEN |\n");
printf("-----+---------+--------+-------+---------+\n");
while(!feof(h264bitstream))
{
int data_lenth;
data_lenth=GetAnnexbNALU(n);
char type_str[20]={0};
switch(n->nal_unit_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;
}
char idc_str[20]={0};
switch(n->nal_reference_idc>>5){
case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str,"DISPOS");break;
case NALU_PRIRITY_LOW:sprintf(idc_str,"LOW");break;
case NALU_PRIORITY_HIGH:sprintf(idc_str,"HIGH");break;
case NALU_PRIORITY_HIGHEST:sprintf(idc_str,"HIGHEST");break;
}
fprintf(myout,"%5d| %8d| %7s| %6s| %8d|\n",nal_num,data_offset,idc_str,type_str,n->len);
data_offset=data_offset+data_lenth;
nal_num++;
}
//Free
if (n){
if (n->buf){
free(n->buf);
n->buf=NULL;
}
free (n);
}
return 0;
}
結果如下:
應用:網頁視頻流m3u8/ts視頻下載
現在很多視頻網站播放流視頻,都不采用mp4/flv文件播放,而是采用m3u8/ts這種方式播放。這種方式既是為了緩解瀏覽器緩存的壓力,也是為了防止視頻泄漏。簡單來說就是,網站后臺把視頻切片成上千個xx.ts文件,一般10s一個,每個都幾百個kb很小,然后通過xx.m3u8
播放列表把這些文件連接起來。
我們直接點擊這個playlist.m3u8
播放列表文件,在旁邊的preview
欄中查看內容,可以看到:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:11
#EXTINF:5.250000,
out000.ts
#EXTINF:9.500000,
out001.ts
#EXTINF:8.375000,
out002.ts
#EXTINF:5.375000,
out003.ts
#EXTINF:9.000000,
out004.ts
...........
這里本來應該有個實戰的,看了一下手下分析的不可描述的網站(逃~),作罷.....
為了下載視頻,得想辦法把所有的ts切片文件下載下來,然后合成一個完整的視頻。因為ts流命名規范有規律,非常容易下載,配合xx.m3u8
播放列表文件,就可以直接用ffmpeg在線下載所有的視頻并合并。直接執行這一句命令:
$ ffmpeg -i <m3u8-path> -c copy OUTPUT.mp4
$ ffmpeg -i <m3u8-path> -vcodec copy -acodec copy OUTPUT.mp4
而m3u8實際上是HLS直播協議,在之后的文章中會介紹到。但是我們完全可以利用VLC播放軟件播放直播源,從而看到視頻。
為什么下載播放列表就能下載所有的切片文件?
因為播放列表里的都是相對路徑,既然我們有了播放列表的絕對路徑,那么其它所有文件的絕對路徑也就不難獲取了。
好在ffmpeg直接實現了這種播放列表一鍵下載的方式。
當然部分網站可能會對視頻流數據進行加密,從而導致ts文件不能合并。但是比如有key的話,同樣可以通過key進行合并視頻。