本文主要從如下幾個方面對AVPacket
作解析:html
查了一些資料,發現FFmpeg的版本更新仍是挺快,並且有不少API也有改動,本文使用的FFmpeg的最新版本3.1。緩存
AVPacket是FFmpeg中很重要的一個數據結構,它保存瞭解複用以後,解碼以前的數據(仍然是壓縮後的數據)和關於這些數據的一些附加信息,如顯示時間戳(pts)、解碼時間戳(dts)、數據時長,所在媒體流的索引等。數據結構
對於視頻(Video)來講,AVPacket一般包含一個壓縮的Frame,而音頻(Audio)則有可能包含多個壓縮的Frame。而且,一個Packet有多是空的,不包含任何壓縮數據,只含有side data(side data,容器提供的關於Packet的一些附加信息。例如,在編碼結束的時候更新一些流的參數)。app
AVPacket的大小是公共的ABI(public ABI)一部分,這樣的結構體在FFmpeg不多,由此也可見AVPacket的重要性。它能夠被分配在棧空間上(可使用語句AVPacket packet;
在棧空間定義一個Packet ),而且除非libavcodec 和 libavformat有很大的改動,否則不會在AVPacket中添加新的字段。less
官方文檔: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.ide
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中的字段可用分爲兩部分:數據的緩存及管理,關於數據的屬性說明。ui
AVPacket實際上可用看做一個容器,它自己並不包含壓縮的媒體數據,而是經過data指針引用數據的緩存空間。因此將一個Packet做爲參數傳遞的時候,妖就要根據具體的須要,對data引用的這部分數據緩存空間進行特殊的處理。當從一個Packet去建立另外一個Packet的時候,有兩種狀況:this
第二種狀況,數據空間的管理比較簡單,可是數據實際上有多個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
。
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中。
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的函數大約有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
代替。
在FFmpeg3:播放音頻中,使用了AVPacket隊列來緩存從流中讀取的幀數據。這就涉及到屢次的AVPacket的傳遞,從流中讀取Packet插入隊列;從隊列中取出Packet進行解碼;以及一些中間變量。因爲Dranger教程中使用的已經廢棄的API,在參照官方文檔進行修改的時候就出現了內存讀寫的異常。下面就播放音頻的教程中的AVPacket隊列實現,分析下在AVPacket做爲參數傳遞的過程當中,應該如何更好的管理其data引用的緩存空間。
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,這樣其就不會被釋放掉。
//*pkt = pktl->pkt; if (av_packet_ref(pkt, &pktl->pkt) < 0) { ret = 0; break; }
註釋掉的代碼仍然是兩個packet引用了同一個緩存空間,這樣在一個使用完成釋放掉緩存的時候,會形成另外一個訪問錯誤。因此扔給調用av_packet_ref
將其引用計數+1,這樣在釋放其中一個packet的時候其引用的數據緩存就不會被釋放掉,知道兩個packet都被釋放。