ffmpeg 中使用 H264 編碼
ffmpeg 已經(jīng)有實現(xiàn)好了編碼器渗磅,調(diào)用的時候指定AV_CODEC_ID_H264
甫菠,會使用 x264 的軟編碼;如果需要硬編碼將查找編碼器改為avcodec_find_encoder_by_name("h264_qsv")
解幼。
int size;
int in_w = mWidth;
int in_h = mHeight;
//x264軟編碼初始化
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if(!pCodec) {
fprintf(stderr, "h264 codec not found");
return false;
}
//qsv硬編碼
// pCodec = avcodec_find_encoder_by_name("h264_qsv");
// if(Q_NULLPTR == pCodec){
// qDebug("avcodec_find_encoder failed!");
// return false;
// }
pCodecCtx = avcodec_alloc_context3(pCodec);
pCodecCtx->codec_id = AV_CODEC_ID_H264;
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
pCodecCtx->width = in_w;
pCodecCtx->height = in_h;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;//幀率
pCodecCtx->bit_rate = mBitRate; //比特率
pCodecCtx->gop_size=25;
···
//讀取編碼后數(shù)據(jù)外永,在AVPacket中
if(0 != avcodec_send_frame(m_pEncodeContext, m_pSwsFrame)) {
qDebug("avcodec_send_frame failed");
return false;
}
while(0 == avcodec_receive_packet(m_pEncodeContext, m_pDstPacket)) {
if(m_pCallback) {
if (m_pDstPacket->flags & AV_PKT_FLAG_KEY) {
qDebug("encode a key frame");
}
m_pCallback->onVideoEncodeFinished(m_pDstPacket->data, m_pDstPacket->size);
}
av_packet_unref(m_pDstPacket);
}
H264 是屬于視頻的編碼層的標準格式,平時用 ffmpeg 只是指定下參數(shù)就完事了匙铡,并沒有仔細了解图甜,今天來分析下 H264 碼流具體的內(nèi)容。
H264 原始碼流結(jié)構(gòu)
在 H.264/AVC 視頻編碼標準中鳖眼,整個系統(tǒng)框架被分為兩層黑毅,VCL(視頻編碼層 video coding layer) 和 NAL(網(wǎng)絡(luò)提取層 network abstraction layer):
- VCL:核心算法引擎,塊钦讳,宏塊及片的語法級別的定義矿瘦,負責高效的視頻內(nèi)容表示,通俗的講就是編碼器直接編碼之后的數(shù)據(jù)愿卒,這部分數(shù)據(jù)還不能直接用于保存和網(wǎng)絡(luò)傳輸缚去,否則在解析上存在困難。
- NAL:負責格式化數(shù)據(jù)并提供頭信息琼开,以保證數(shù)據(jù)適合各種信道和存儲介質(zhì)上的傳輸易结,通俗的講 NAL 就是 VCL 加了一些頭部信息封裝了一下。
H264 碼流數(shù)據(jù)由 NALU 序列組成,相鄰的 NALU 由起始碼 StartCode 隔開搞动,起始碼 StartCode 的兩種形式:3 字節(jié)的 0x000001 和 4 字節(jié)的 0x00000001躏精。3 字節(jié)的 起始碼只有在一個完整的幀被編為多個 slice(片)的時候,從第二個 slice 開始滋尉,使用 3 字節(jié)起始碼玉控。
NALU 是一個 NAL 單元,NALU = 字節(jié)的頭信息(NALU Header) + 原始字節(jié)序列載荷(RBSP)(實際為擴展字節(jié)序列載荷(EBSP)的字節(jié)流)
NALU Header
NALU Header 由 3 部分組成狮惜,NALU Header = forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)高诺,共占用 1 個字節(jié)
- forbidden_bit 禁止位:編碼中默認值為 0,當網(wǎng)絡(luò)識別此單元中存在比特錯誤時碾篡,可將其設(shè)為 1虱而,以便接收方丟掉該單元,主要用于適應(yīng)不同種類的網(wǎng)絡(luò)環(huán)境
- nal_reference_bit 重要性指示位:用于在重構(gòu)過程中標記一個 NAL 單元的重要性开泽,值越大牡拇,越重要。尤其是當前 NALU 為圖像參數(shù)集穆律、序列參數(shù)集或 IDR 圖像時惠呼,或者為參考圖像條帶(片/Slice),或者為參考圖像的條帶數(shù)據(jù)分割時峦耘,nal_ref_idc 值肯定不為 0剔蹋。
- nal_unit_type:NALU 類型位: 可以表示 NALU 的 32 種不同類型特征,類型 1 ~ 12 是 H.264 定義的辅髓,類型 24 ~ 31 是用于 H.264 以外的泣崩,RTP 負荷規(guī)范使用這其中的一些值來定義包聚合和分裂,其他值為 H.264 保留洛口。
判斷起始碼后的第一個字節(jié)的后 5bit 就可以判斷出這個 NALU 的類型矫付,例如:00 00 00 01 67: 0x67&0x1f = 0x07 :SPS
。另外說下重要的類型:
- al_unit_type=5:表示當前 NAL 是 IDR 圖像的一個片第焰,在這種情況下买优,IDR 圖像中的每個片的 nal_unit_type 都應(yīng)該等于 5。注意挺举,IDR 圖像不能使用分區(qū)杀赢。
- nal_unit_type=7:SPS,包括一個圖像序列的所有信息豹悬,即兩個IDR圖像間的所有圖像信息葵陵,如標識符 seq_parameter_set_id、幀數(shù)及 POC 的約束瞻佛、參考幀數(shù)目脱篙、解碼圖像尺寸和幀場編碼模式選擇標識等等娇钱。
- nal_unit_type=8:PPS,PPS對應(yīng)的是一個序列中某一幅圖像或者某幾幅圖像绊困,包括一個圖像的所有slice的所有相關(guān)信息文搂,如圖像類型、序列號秤朗、標識符 pic_parameter_set_id煤蹭、可選的 seq_parameter_set_id、熵編碼模式選擇標識取视、片組數(shù)目硝皂、初始量化參數(shù)和去方塊濾波系數(shù)調(diào)整標識等等,解碼時某些序列號的丟失可用來校驗信息包的丟失與否作谭。
RBSP
NALU 的主體部分需要先介紹下面 3 個名詞稽物。
- SODB: String Of Data Bits 原始數(shù)據(jù)比特流,編碼后的原始數(shù)據(jù)折欠,即VCL數(shù)據(jù)贝或。
- RBSP: 原始字節(jié)序列載荷。由于 SODB 長度不一定是 8 的倍數(shù)锐秦,為了字節(jié)對齊咪奖,在 SODB 的后面填加了結(jié)尾比特(rbsp_trailing_bits)就得到了 RBSP。rbsp_trailing_bits = rbsp_stop_one_bit(值為 1酱床,1bit)+ rbsp_alignment_zero_bit(若干個 0羊赵,字節(jié)對齊填充)
- EBSP: 擴展字節(jié)序列載荷。NALU 之間是通過 StartCode 來隔開的斤葱,如果編碼后的原始數(shù)據(jù)含有 0x000001慷垮,就無法知道這個到底是不是 StartCode揖闸,因此為了使 NALU 主體中不包括與開始碼相沖突的數(shù)據(jù)揍堕,在 RBSP 數(shù)據(jù)中每遇到兩個字節(jié)連續(xù)為 0,就插入一個字節(jié)的 0x03汤纸,這樣就得到了 EBSP衩茸。
所以,SODB + 添加尾部字節(jié)對齊 ==> RBSP + 碰到 0x0000 插入 0x03 ==> EBSP贮泞。
VCL層是對核心算法引擎楞慈、塊、宏塊及片的語法級別的定義啃擦,最終輸出壓縮編碼后的數(shù)據(jù) SODB囊蓝。VCL數(shù)據(jù)在傳輸或存儲之前,先被映射或封裝進NAL單元中令蛉。NAL層將SODB打包成RBSP然后加上NALU Header聚霜,組成一個NALU狡恬。
相應(yīng)的 RBSP 數(shù)據(jù)類型描述如下:
視頻幀
從宏觀上來說,SPS蝎宇、PPS弟劲、IDR 幀(包含一個或多個 I-Slice)、P 幀(包含一個或多個 P-Slice )姥芥、B 幀(包含一個或多個 B-Slice )組成的視頻序列構(gòu)成了 H264 的碼流兔乞。
包含具體視頻數(shù)據(jù)的幀有:
類型 | 意義 |
---|---|
I 幀 | I 幀通常是每個 GOP(MPEG 所使用的一種視頻壓縮技術(shù))的第一個幀,經(jīng)過適度地壓縮凉唐,做為隨機訪問的參考點庸追,可以當成圖象。 |
P 幀 | 通過充分將低于圖像序列中前面已編碼幀的時間冗余信息來壓縮傳輸數(shù)據(jù)量的編碼圖像台囱,也叫預(yù)測幀 |
B 幀 | 既考慮與源圖像序列前面已編碼幀锚国,也顧及源圖像序列后面已編碼幀之間的時間冗余信息來壓縮傳輸數(shù)據(jù)量的編碼圖像,也叫雙向預(yù)測幀 |
一個視頻序列的第一幀又叫 IDR 幀。IDR 幀的作用是立刻刷新玄坦,使錯誤不致傳播血筑,從 IDR 幀開始,重新算一個新的序列開始編碼煎楣。
I 幀可以不依賴其他幀就解碼出一幅完整的圖像豺总,而 P 幀、B 幀不行择懂。P 幀需要依賴視頻流中排在它前面的幀才能解碼出圖像喻喳,B 幀則需要依賴視頻流中排在它前面或后面的幀才能解碼出圖像。在視頻流中困曙,先到來的 B 幀無法立即解碼表伦,需要等待它依賴的后面的 P 幀先解碼,播放時間與解碼時間不一致慷丽,這時就需要DTS和PTS了蹦哼。
- DTS(Decoding Time Stamp):解碼時間戳,這個時間戳的意義在于告訴播放器該在什么時候解碼這一幀的數(shù)據(jù)要糊。
- PTS(Presentation Time Stamp):顯示時間戳纲熏,這個時間戳用來告訴播放器該在什么時候顯示這一幀的數(shù)據(jù)。
在沒有B幀的時候锄俄,DTS局劲、PTS順序是一致的,比如直播等延遲要求小的使用場景奶赠。音頻也有PTS鱼填、DTS,但是音頻沒有類似視頻的 B 幀毅戈,不需要雙向預(yù)測苹丸,所以音頻幀的 DTS塑猖、PTS 順序是一致的。
slice
幀圖像可編碼成一個或者多個片(slice)谈跛,分片的目的是為了限制誤碼的擴散和傳輸羊苟,使編碼片相互間保持獨立。
每一個 slice 總體來看都由兩部分組成感憾,一部分作為 slice header蜡励,用于保存 slice 的總體信息(如當前 slice 的類型等),另一部分為 slice body阻桅,通常是一組連續(xù)的宏塊結(jié)構(gòu)凉倚。
slice 的類型如下:
類型 | 意義 |
---|---|
I slice | 只包含 I 宏塊 |
P slice | 包含 P 和 I 宏塊 |
B slice | 包含 B 和 I 宏塊 |
SP slice | 包含 P 或 I 宏塊,用于不同碼流之間的切換 |
SI slice | 一種特殊類型的編碼宏塊 |
宏塊
宏塊是視頻信息的主要承載者。一個編碼圖像通常劃分為多個宏塊組成.包含著每一個像素的亮度和色度信息嫂沉。視頻解碼最主要的工作則是提供高效的方式從碼流中獲得宏塊中像素陣列稽寒。
一個宏塊由一個 16×16 亮度像素和附加的一個 8×8 Cb 和一個 8×8 Cr 彩色像素塊組成。宏塊分類如下:
類型 | 意義 |
---|---|
I 宏塊 | 利用從當前片中已解碼的像素作為參考進行幀內(nèi)預(yù)測 |
P 宏塊 | 利用前面已編碼圖像作為參考進行幀內(nèi)預(yù)測 |
B 宏塊 | 利用雙向的參考圖像(當前和未來的已編碼圖像幀)進行幀內(nèi)預(yù)測 |
H264 解析流程
拿到 RBSP 或 SODB 之后趟章,根據(jù)對應(yīng)的 NALU Header 類型來解析編碼的數(shù)據(jù)杏糙,解析流程如下:
使用 ffmpeg 的話一般是不會自己去解析的,因為數(shù)據(jù)都在對應(yīng)的結(jié)構(gòu)體變量中蚓土,比如判斷一幀是否是 I 幀:
if (m_pDstPacket->flags & AV_PKT_FLAG_KEY) {
qDebug("encode a key frame");
}