本文主要從以下幾個方面對AVPacket
做解析:
- AVPacket在FFmpeg中的作用
- 字段說明
- AVPacket中的內存管理
- AVPacket相關函數的說明
- 結合AVPacket隊列說明下AVPacket在傳遞過程中數據緩存的管理
查了一些資料,發現FFmpeg的版本更新還是挺快,而且有很多API也有改動,本文使用的FFmpeg的最新版本3.1。
AVPacket簡介
AVPacket是FFmpeg中很重要的一個數據結構,它保存了解復用之后,解碼之前的數據(仍然是壓縮后的數據)和關於這些數據的一些附加信息,如顯示時間戳(pts)、解碼時間戳(dts)、數據時長,所在媒體流的索引等。
對於視頻(Video)來說,AVPacket通常包含一個壓縮的Frame,而音頻(Audio)則有可能包含多個壓縮的Frame。並且,一個Packet有可能是空的,不包含任何壓縮數據,只含有side data(side data,容器提供的關於Packet的一些附加信息。例如,在編碼結束的時候更新一些流的參數)。
AVPacket的大小是公共的ABI(public ABI)一部分,這樣的結構體在FFmpeg很少,由此也可見AVPacket的重要性。它可以被分配在棧空間上(可以使用語句AVPacket packet;
在棧空間定義一個Packet ),並且除非libavcodec 和 libavformat有很大的改動,不然不會在AVPacket中添加新的字段。
官方文檔:AVPacket is one of the few structs in FFmpeg,whose size is a part of public ABI.Thus it may be allocated on stack and no new fields can be added to it without libavcodec and libavformat major bump.
AVPacket 字段說明
AVPacket的聲明在avcodec.h
中,其聲明如下:
typedef struct AVPacket {
/**
* A reference to the reference-counted buffer where the packet data is
* stored.
* May be NULL, then the packet data is not reference-counted.
*/
AVBufferRef *buf;
/**
* Presentation timestamp in AVStream->time_base units; the time at which
* the decompressed packet will be presented to the user.
* Can be AV_NOPTS_VALUE if it is not stored in the file.
* pts MUST be larger or equal to dts as presentation cannot happen before
* decompression, unless one wants to view hex dumps. Some formats misuse
* the terms dts and pts/cts to mean something different. Such timestamps
* must be converted to true pts/dts before they are stored in AVPacket.
*/
int64_t pts;
/**
* Decompression timestamp in AVStream->time_base units; the time at which
* the packet is decompressed.
* Can be AV_NOPTS_VALUE if it is not stored in the file.
*/
int64_t dts;
uint8_t *data;
int size;
int stream_index;
/**
* A combination of AV_PKT_FLAG values
*/
int flags;
/**
* Additional packet data that can be provided by the container.
* Packet can contain several types of side information.
*/
AVPacketSideData *side_data;
int side_data_elems;
/**
* Duration of this packet in AVStream->time_base units, 0 if unknown.
* Equals next_pts - this_pts in presentation order.
*/
int64_t duration;
int64_t pos; ///< byte position in stream, -1 if unknown
#if FF_API_CONVERGENCE_DURATION
/**
* @deprecated Same as the duration field, but as int64_t. This was required
* for Matroska subtitles, whose duration values could overflow when the
* duration field was still an int.
*/
attribute_deprecated
int64_t convergence_duration;
#endif
} AVPacket;
AVPacket中的字段可用分為兩部分:數據的緩存及管理,關於數據的屬性說明。
- 關於數據的屬性有以下字段:
- pts 顯示時間戳
- dts 解碼時間戳
- stream_index Packet所在stream的index
- flats 標志,其中最低為1表示該數據是一個關鍵幀
- duration 數據的時長,以所屬媒體流的時間基准為單位
- pos 數據在媒體流中的位置,未知則值為-1
- convergence_duration 該字段已被deprecated,不再使用
- 數據緩存,AVPacket本身只是個容器,不直接的包含數據,而是通過數據緩存的指針引用數據。AVPacket中包含有兩種數據
- data 指向保存壓縮數據的指針,這就是AVPacket實際的數據。
- side_data 容器提供的一些附加數據
- buf 是AVBufferRef類型的指針,用來管理data指針引用的數據緩存的,其使用在后面介紹。
AVPacket中的內存管理
AVPacket實際上可用看作一個容器,它本身並不包含壓縮的媒體數據,而是通過data指針引用數據的緩存空間。所以將一個Packet作為參數傳遞的時候,妖就要根據具體的需要,對data引用的這部分數據緩存空間進行特殊的處理。當從一個Packet去創建另一個Packet的時候,有兩種情況:
- 兩個Packet的data引用的是同一數據緩存空間,這時候要注意數據緩存空間的釋放問題
- 兩個Packet的data引用不同的數據緩存空間,每個Packet都有數據緩存空間的copy。
第二種情況,數據空間的管理比較簡單,但是數據實際上有多個copy造成內存空間的浪費。所以要根據具體的需要,來選擇到底是兩個Packet共享一個數據緩存空間,還是每個Packet擁有自己獨自的緩存空間。
對於多個Packet共享同一個緩存空間,FFmpeg使用的引用計數的機制(reference-count)。當有新的Packet引用共享的緩存空間時,就將引用計數+1;當釋放了引用共享空間的Packet,就將引用計數-1;引用計數為0時,就釋放掉引用的緩存空間。
AVPacket中的AVBufferRef *buf;
就是用來管理這個引用計數的,AVBufferRef
的聲明如下:
typedef struct AVBufferRef {
AVBuffer *buffer;
/**
* The data buffer. It is considered writable if and only if
* this is the only reference to the buffer, in which case
* av_buffer_is_writable() returns 1.
*/
uint8_t *data;
/**
* Size of data in bytes.
*/
int size;
} AVBufferRef;
在AVPacket中使用AVBufferRef
有兩個函數:av_packet_ref
和av_packet_unref
。
- av_packet_ref
int av_packet_ref(AVPacket *dst, const AVPacket *src)
創建一個src->data
的新的引用計數。如果src已經設置了引用計數發(src->buffer不為空),則直接將其引用計數+1;如果src沒有設置引用計數(src->buffer為空),則為dst創建一個新的引用計數buf,並復制src->data
到buf->buffer
中。最后,復制src的其他字段到dst中。
- av_packet_unref
void av_packet_unref(AVPacket *pkt)
將緩存空間的引用計數-1,並將Packet中的其他字段設為初始值。如果引用計數為0,自動的釋放緩存空間。
所以,有兩個Packet共享同一個數據緩存空間的時候可用這么做
av_read_frame(pFormatCtx, &packet) // 讀取Packet
av_packet_ref(&dst,&packet) // dst packet共享同一個數據緩存空間
...
av_packet_unref(&dst);
下一小節簡單的介紹下AVPacket相關的函數,並介紹如何在傳遞Packet的時候,復制一個獨立的數據緩存空間的copy,每個Packet都擁有自己獨立的數據緩存空間。
AVPacket 相關函數介紹
操作AVPacket的函數大約有30個,主要可以分為:AVPacket的創建初始化、AVPacket中的data數據管理(clone,free,copy等)、AVPacket中的side_data數據管理。
AVPacket的創建有很多種,而由於Packet中的數據是通過data引用的,從一個Packet來創建另一個Packet有多種方法。
av_read_frame
這個是比較常見的了,從媒體流中讀取幀填充到填充到Packet的數據緩存空間。如果Packet->buf
為空,則Packet的數據緩存空間會在下次調用av_read_frame
的時候失效。這也就是為何在FFmpeg3:播放音頻中,從流中讀取到Packet的時,在將該Packet插入隊列時,要調用av_dup_avpacket
重新復制一份緩存數據。(av_dup_avpacket
函數已廢棄,后面會介紹)av_packet_alloc
創建一個AVPacket,將其字段設為默認值(data為空,沒有數據緩存空間)。av_packet_free
釋放使用av_packet_alloc
創建的AVPacket,如果該Packet有引用計數(packet->buf不為空),則先調用av_packet_unref(&packet)
。av_packet_clone
其功能是av_packet_alloc
+av_packet_ref
av_init_packet
初始化packet的值為默認值,該函數不會影響data引用的數據緩存空間和size,需要單獨處理。av_new_packet
av_init_packet
的增強版,不但會初始化字段,還為data分配了存儲空間。av_copy_packet
復制一個新的packet,包括數據緩存。av_packet_from_data
初始化一個引用計數的packet,並指定了其數據緩存。av_grow_packet
和av_shrink_packet
增大或者減小Packet->data指向的數據緩存。
就羅列這么多吧,剩下的沒提到的基本都是和side_data相關的一些函數,和data的比較類似。
最后介紹下已經廢棄的兩個函數 av_dup_packet
和av_free_packet
。
av_dup_packet
是復制src->data引用的數據緩存,賦值給dst。也就是創建兩個獨立packet,這個功能現在可用使用函數av_packet_ref
來代替。
av_free_packet
釋放packet,包括其data引用的數據緩存,現在可以使用av_packet_unref
代替。
AVPacket隊列
在FFmpeg3:播放音頻中,使用了AVPacket隊列來緩存從流中讀取的幀數據。這就涉及到多次的AVPacket的傳遞,從流中讀取Packet插入隊列;從隊列中取出Packet進行解碼;以及一些中間變量。由於Dranger教程中使用的已經廢棄的API,在參照官方文檔進行修改的時候就出現了內存讀寫的異常。下面就播放音頻的教程中的AVPacket隊列實現,分析下在AVPacket作為參數傳遞的過程中,應該如何更好的管理其data引用的緩存空間。
- 從流中讀取AVPacket插入隊列
AVPacket packet;
while (av_read_frame(pFormatCtx, &packet) >= 0)
{
if (packet.stream_index == audioStream)
packet_queue_put(&audioq, &packet);
else
//av_free_packet(&packet);
av_packet_unref(&packet);
}
如果是音頻流則將讀到Packet調用packet_queue_put
插入到隊列,如果不是音頻流則調用av_packet_unref
釋放已讀取到的AVPacket數據。
下面代碼是packet_queue_put
中將Packet放入到一個新建的隊列節點的代碼片段
AVPacketList *pktl;
//if (av_dup_packet(pkt) < 0)
//return -1;
pktl = (AVPacketList*)av_malloc(sizeof(AVPacketList));
if (!pktl)
return -1;
if (av_packet_ref(&pktl->pkt, pkt) < 0)
return -1;
//pktl->pkt = *pkt;
pktl->next = nullptr;
注意,在調用packet_queue_put
時傳遞的是指針,也就是形參pkt和實參packet中的data引用的是同一個數據緩存。但是在循環調用av_read_frame
的時候,會將packet中的data釋放掉,以便於讀取下一個幀數據。
所以就需要對data引用的數據緩存進行處理,保證在讀取下一個幀數據的時候,其data引用的數據空間沒有被釋放。有兩種方法,復制一份data引用的數據緩存或者給data引用的緩存空間加一個引用計數。
注釋掉的部分是使用已廢棄的APIav_dup_packet
,該函數將pkt中data引用的數據緩存復制一份給隊列節點中的AVPacket。
添加引用計數的方法則是調用av_apcket_ref
將data引用的數據緩存的引用計數+1,這樣其就不會被釋放掉。
- 從隊列中取出AVPacket
//*pkt = pktl->pkt;
if (av_packet_ref(pkt, &pktl->pkt) < 0)
{
ret = 0;
break;
}
注釋掉的代碼仍然是兩個packet引用了同一個緩存空間,這樣在一個使用完成釋放掉緩存的時候,會造成另一個訪問錯誤。所以扔給調用av_packet_ref
將其引用計數+1,這樣在釋放其中一個packet的時候其引用的數據緩存就不會被釋放掉,知道兩個packet都被釋放。