H.264媒體流AnnexB和AVCC格式分析 及 FFmpeg解析mp4的H.264碼流方法


H264碼流分兩種組織方式,一種是AnnexB格式,一種是AVCC格式。

首先要了解的是H.264編碼規范只是規定了如何編碼,並沒有規定以何種方式來排列編碼后的數據。就如同AES算法只是規定如何加密一組數據,並沒有強制規定如果分組。H.264規范沒有規定如何組織數據,但是在附錄B中提供了一種可選方案,即Annex B格式。

H.264 NALU 概念

H.264視頻編碼后的數據叫NALU(Network Abstraction Layer Units)
NALU有多種類型,分為兩大類:VCL(Video Coding Layer)非VCL。VCL是圖像編碼數據,非VCL為編碼參數信息。NALU結構頭部指明類型,類型字段如下。

NALU類型 說明 是否VCL
0 Unspecified non-VCL
1 Coded slice of a non-IDR picture VCL
2 Coded slice data partition A VCL
3 Coded slice data partition B VCL
4 Coded slice data partition C VCL
5 Coded slice of an IDR picture VCL
6 Supplemental enhancement information (SEI) non-VCL
7 Sequence parameter set (SPS) non-VCL
8 Picture parameter set (PPS) non-VCL
9 Access unit delimiter non-VCL
10 End of sequence non-VCL
11 End of stream non-VCL
12 Filler data non-VCL
13 Sequence parameter set extension non-VCL
14 Prefix NAL unit non-VCL
15 Subset sequence parameter set non-VCL
16 Depth parameter set non-VCL
17…18 Reserved non-VCL
19 Coded slice of an auxiliary coded picture without partitioning non-VCL
20 Coded slice extension non-VCL
21 Coded slice extension for depth view components non-VCL
22…23 Reserved non-VCL
24…31 Unspecified non-VCL

SPS: 序列參數集,包含解碼配置,比如profile level 分辨率和幀率等。
PPS:圖像參數集,包含有關熵編碼模式、分片組、運動預測和去塊濾波器等信息。
IDR: 立即解碼刷新單元,這種NALU包含一個完整的圖像序列,不依賴其他NALU就可以獨立解碼和顯示,即一種特殊的I幀。

值得注意的是,一個NALU即使是VCL NALU 也並不一定表示一個視頻幀。因為一個幀的數據可能比較多,可以分片為多個NALU來儲存。一個或者多個NALU組成一個訪問單元AU,一個AU包含一個完整的幀。

H.264 碼流格式

H264碼流分兩種組織方式,一種是AnnexB格式,一種是AVCC格式。

AnnexB格式

[start code]NALU | [start code] NALU |...
這種格式比較常見,也就是我們熟悉的每個幀前面都有0x00 00 00 01或者0x00 00 01作為起始碼。.h264文件就是采用的這種格式,每個幀前面都要有個起始碼。SPS PPS等也作為一類NALU存儲在這個碼流中,一般在碼流最前面。也就是說這種格式包含VCL 和 非VCL 類型的NALU。

AVCC格式

([extradata]) | ([length] NALU) | ([length] NALU) | ...
這種模式也叫AVC1格式,沒有起始碼,每個幀最前面幾個字節(通常4字節)是幀長度。這里的NALU一般沒有SPS PPS等參數信息,參數信息屬於額外數據extradata存在其他地方。
比如ffmpeg中解析mp4文件后sps pps存在streams[index]->codecpar->extradata;中。也就是說這種碼流通常只包含VCL類型NALU。

這些extradata通常有如下格式(可以根據這個規則ffmpeg解析mp4文件的SPS和PPS)

第1字節:version (通常0x01)
第2字節:avc profile (值同第1個sps的第2字節)
第3字節:avc compatibility (值同第1個sps的第3字節)
第4字節:avc level (值同第1個sps的第3字節)
第5字節前6位:保留全1
第5字節后2位:NALU Length 字段大小減1,通常這個值為3,即NAL碼流中使用3+1=4字節表示NALU的長度
第6字節前3位:保留,全1
第6字節后5位:SPS NALU的個數,通常為1
第7字節開始后接1個或者多個SPS數據
	SPS結構 [16位 SPS長度][SPS NALU data]

SPS數據后
第1字節:PPS的個數,通常為1
第2字節開始接1個或多個PPS數據
	PPS結構 [16位 PPS長度][SPS NALU data]

上述內容參考 https://titanwolf.org/Network/Articles/Article?AID=7efa4423-1e7d-46e0-ba19-6f5c6eec84a7

FFmpeg解析mp4中H.264 碼流

MP4文件中編碼信息是存儲在文件開始或者文件末尾的,詳細結構這里不詳述了。就知道不是和圖像數據放在一起的就可以了。
FFmpeg使用av_read_frame(AVFormatContext *s, AVPacket *pkt)函數讀mp4文件,讀到packet里面僅僅是VCL編碼數據NAL,並且這個編碼數據是AVCC格式組織的碼流,直接保存成.264文件沒法播放。
先說一下思路
1 .從avFmtCtx->streams[_videoStreamIndex]->codecpar->extradata中解析SPS和PPS數據,數據格式上一節已經描述了。解析出SPS PPS數據加上4字節的0001的起始碼拼裝成nnexB格式的NALU,先寫入文件。
2. 通過av_read_frame(AVFormatContext *s, AVPacket *pkt)讀取到數據存放在pkt->data中,長度為pkt->size

注意:這1個pkt->data中的數據可能是多個NALU的數據!!!這些數據按([length] NALU) | ([length] NALU) | ...規則排列。先取前4字節作為長度,讀取指定長度的數據加上起始碼拼NALU。然后同樣的方式讀取后面的數據,直到總長度等於pkt->size

FFmpeg 解析mp4中H264碼流 代碼示例

這里就只貼關鍵部分代碼。省略前面打開文件和查詢流信息等操作。

	//...
    AVPacket spsPacket, ppsPacket, tmpPacket;
    uint8_t startCode[4] = {0x00, 0x00, 0x00, 0x01};
    bool sendSpsPps = false;

    while (av_read_frame(_avFmtCtx, _avPacket) == 0) { // 能讀到數據返回0,循環讀取
    	// 根據pkt->stream_index判斷是不是視頻流
        if (_avPacket->stream_index == _videoStreamIndex) {
        	// 僅1次處理sps pps,也可以拿在while外面
            if (!sendSpsPps) { 
                int spsLength = 0;
                int ppsLength = 0;
                // extradata 數據指針,方便操作取其指針
                uint8_t *ex = _avFmtCtx->streams[_videoStreamIndex]->codecpar->extradata;
				
				// extradata;第6字節后5位表示SPS個數,通常為1,這里就省略判斷處理,嚴謹期間還是要判斷
				// 直接 取第7 8 倆字節作為SPS長度
                spsLength = (ex[6] << 8) | ex[7];
				
				// x[8+spsLength]表示PPS個數,通常為1,這里就省略判斷處理
				// 取接下來兩位作為PPS長度
                ppsLength = (ex[8 + spsLength + 1] << 8) | ex[8 + spsLength + 2];

				// 為spsPacket ppsPacket的data分配內存,類似malloc
				// 如果只是為了保存文件,可以不使用pkt結構,直接malloc就行
				// 分配的空間為sps或pps長度加上4字節的起始碼
                av_new_packet(&spsPacket, spsLength + 4);
                av_new_packet(&ppsPacket, ppsLength + 4);
				
				// 給SPS拼前4字節起始碼
                memcpy(spsPacket.data, startCode, 4);
                // 把SPS數據拼在起始碼后面
                memcpy(spsPacket.data + 4, ex + 8, spsLength);
				
				// TODO: 這里可以把spsPacket.data數據寫入文件 
				
				// 給PPS拼前4字節起始碼
                memcpy(ppsPacket.data, startCode, 4);
                // 把PPS數據拼在起始碼后面
                memcpy(ppsPacket.data + 4, ex + 8 + spsLength + 2 + 1, ppsLength);
                
				// TODO: 這里可以把ppsPacket.data數據寫入文件 
				
                sendSpsPps = true;
            }

			// 下面處理讀到pkt中的數據
            int nalLength = 0;
            uint8_t *data = _avPacket->data;
            // _avPacket->data中可能有多個NALU,循環處理
            while (data < _avPacket->data + _avPacket->size) {
            	// 取前4字節作為nal的長度
                nalLength = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
                if (nalLength > 0) {
                    memcpy(data, startCode, 4);  // 拼起始碼
                    tmpPacket = *_avPacket;      // 僅為了復制packet的其他信息,保存文件可忽略
                    tmpPacket.data = data;		 // 把tmpPkt指針偏移到實際數據位置
                    tmpPacket.size = nalLength + 4; // 長度為nal長度+起始碼4
					
					//TODO: 處理這個NALU的數據,可以直接把tmpPacket.data寫入文件
                }
                data = data + 4 + nalLength; // 處理data中下一個NALU數據
            }
        }

        av_packet_unref(_avPacket);
    }

上述代碼FFmpeg 4.4使用正常。
此外,FFmpeg也提供了h264_mp4toannexb_filter相關的過濾器進行相應轉換的操作,filter`使用比較麻煩,這里不示例怎么用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM