FFmpeg數據結構:AVPacket解析

本文主要從如下幾個方面對AVPacket作解析:html

  • 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的一些附加信息。例如,在編碼結束的時候更新一些流的參數)。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 字段說明

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

  • 關於數據的屬性有如下字段:
    • 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的時候,有兩種狀況:this

  • 兩個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_refav_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->databuf->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_packetav_shrink_packet 增大或者減少Packet->data指向的數據緩存。

就羅列這麼多吧,剩下的沒提到的基本都是和side_data相關的一些函數,和data的比較相似。
最後介紹下已經廢棄的兩個函數 av_dup_packetav_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都被釋放。

相關文章
相關標籤/搜索