8、關於FFmpeg須要絮叨的一些事

[TOC]緩存

開始前的BB

想來想去,感受有些關於FFmpeg的細節和複雜的地方我jio的仍是要單獨給你們講講的,畢竟以前碰到的時候也是花了點功夫才理解,再次舉幾個栗子bash

  1. FFmpeg的Log機制
  2. FFmpeg裏的時間計算
  3. FFmpeg的內存模型
  4. 播放器框架流程(簡版)

這章講的也是在播放器開發中比較重要的知識,但願你們能仔細看,若是有不懂的,能夠加我微信或者微信羣進行交流,要開始了微信

開始

FFmpeg的Log機制

FFmpeg中日誌的話主要的方法就是av_log(),在libavutil\log.h裏,咱們來看一下他的方法框架

/**
* @param avcl  包含一個AVClass的結構體
* @param level 錯誤等級
* @param fmt   拋出帶有format信息的日誌信息
* @param ...   fmt中所須要的數據
**/
void av_log(void *avcl, int level, const char *fmt, ...) 

複製代碼

Log的等級有如下幾個ide

//不打印輸出
#define AV_LOG_QUIET -8

//崩潰性的錯誤
#define AV_LOG_PANIC 0

//出現沒法恢復的問題,好比找不到相應格式的header,或者是傳入了非法的參數
#define AV_LOG_FATAL 8

//出現了問題,沒法恢復數據
#define AV_LOG_ERROR 16

//有點問題,可是可能不影響
#define AV_LOG_WARNING 24

//輸出標準的信息
#define AV_LOG_INFO 32

//輸出更詳細的信息
#define AV_LOG_VERBOSE 40

//輸出libav*裏面的debug信息 對開發者有用
#define AV_LOG_DEBUG 48

//很是冗長的調試,對libav*開發很是有用。
#define AV_LOG_TRACE 56

複製代碼

對於Log的輸出,咱們能夠用函數

int av_log_get_level(void);

void av_log_set_level(int level);
複製代碼

進行set和get操做來控制和獲取日誌的等級測試

輸出log的方法,就是利用void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));這個方法,設置一個函數指針,進行回調。ui

FFmpeg中的時間計算

咱們在ffmpeg中時間主要是計算的PTS的時間 經常使用的時間分爲三種:this

  1. seconds 秒
  2. microsecond 微秒
  3. 自定義時間(音頻的pts中可能會用到)

這三種時間單位都有不一樣的用處,可是均可以進行互相換算。spa

怎麼肯定ffmpeg中使用的是以什麼時間爲基準的呢?

ffmpeg內部有個定義的宏#define AV_TIME_BASE 1000000,這個宏定義了它的時間基 (1s = AV_TIME_BASE),像這裏就是用的微秒(us)作爲時間基

還有另外一個AV_TIME_BASE_Q,這個宏的定義完整的是這樣的

#define AV_TIME_BASE_Q (AVRational){1,AV_TIME_BASE}
複製代碼

這個宏是AV_TIME_BASE的分數表示 也就是 1/AV_TIME_BASE

他們之間的轉換關係是

timestamp(時間戳) = AV_TIME_BASE * time(秒)

time(秒) = AV_TIME_BASE_Q * timestamp(時間戳)
複製代碼

在這裏,細心的同窗會發現 AVRational 這個結構體在不少地方都用到了,這個結構體的全貌是這樣的

/**
 * Rational number (pair of numerator and denominator).
 */
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational
複製代碼

它就是簡單的記錄了一下分子和分母,咱們在ffmpeg中能夠直接用方法

/**
 * Convert an AVRational to a `double`.
 * @param a AVRational to convert
 * @return `a` in floating-point form
 * @see av_d2q()
 */
static inline double av_q2d(AVRational a){
    return a.num / (double) a.den;
}
複製代碼

直接進行計算,就像這樣

timestamp(秒 = pts * av_q2d(stream->time_base);
複製代碼

就能計算出如今這幀的真實pts是多少秒

在FFmpeg中存在的多個時間基(time_base) (沒有錯,就是這麼坑),它們對應着不一樣的階段,每一個time_base的具體值不同,常見的有

  1. AVFormatContext
    1. durtion 整個碼流的時長,獲取正常的時長的時候須要去除以AV_TIME_BASE,單位是秒
  2. AVStream
    1. time_base 單位是秒
    2. duration 表示當前數據流的時長,以time_base爲單位
  3. AVPacket
    1. pts 以AVStream中的time_base爲單位
    2. dts 以AVStream中的time_base爲單位
    3. duration 以AVStream中的time_base爲單位
  4. AVFrame
    1. pts 以AVStream中的time_base爲單位
    2. pkt_dts 以AVStream中的time_base爲單位
    3. duration 以AVStream中的time_base爲單位

注意 在使用解碼中AVFrame的pts的時候,能夠作個時間矯正 frame->pts = frame->best_effort_timestamp 這個值通常狀況下個pts是同樣的,可是在某些狀況下,好比丟幀,會進行一些糾正

FFmpeg的內存模型

內存模型的話咱們這小節主要講AVPacketAVFrame這兩個結構體,由於在播放器的開發中,咱們操做的最多的就是這兩個結構體,一旦處理很差,發生內存泄漏,顯示不正常什麼的。。很刺激的。。。

進入正題

首先,稍微來看一些AVPacket這個結構體

typedef struct AVPacket {
    /**
     * 帶有引用計數buffer,多是空的
     */
    AVBufferRef *buf;
    
    /**
     * 當前Packet的pts
     */
    int64_t pts;
    
    /**
     * 當前Packet的dts
     */
    int64_t dts;
    uint8_t *data;
    int   size;
    
    /**
    * 當前Packet的流下標
    **/
    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;

    /**
     * 當前的packet的持續時間
     */
    int64_t duration;

    int64_t pos;                            ///< 流中的字節位置,若是未知,則爲-1

#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;
複製代碼

AVPacet 是一個解封裝以後的數據(H264,AAC),假如咱們要對這個AVPacket進行拷貝的操做,那麼這個時候就須要注意了

  1. 兩個AVPacket引用的是統一數據的緩存空間,假如釋放一個,另外一個也會被釋放
  2. 兩個AVPacketbuf引用不一樣的數據緩存空間,每一個AVPacket都有數據緩存的拷貝

分配的時候是不會分配buf的

咱們來簡單的測試一下 ,新建chapter_08/AVPacketMemoryModel (爲何又採用C++的寫法了?沒辦法,路子就是這麼野) 頭文件裏

而後實現方法

運行以後咱們發現

至於他的緣由你們能夠去看一下AVPacket *av_packet_alloc(void)方法,一看就知道爲何了

那麼何時纔回去把數據放進去呢?

沒有錯就是調用av_read_fram的方法的時候,纔會去對這個數據賦值

AVPacket數據共享模型,能夠用下面這個圖

對於多個AVPacket共享同一個緩存空間,ffmpeg採用引用計數機制(reference-count)

  1. 初始化時引用計數爲1
  2. 當有新的Packet引用共享的緩存控件時,將引用計數+1
  3. 當釋放了引用共享控件的Packet,就將引用計數-1,引用計數爲0時,就釋放掉緩存空間

(AVFrame表示我也是這麼作的)

閉上眼,仔細感覺 有沒有點智能指針的味道

基於上面的引用機制,咱們在調用AVPacket相關的方法時,就須要注意引用問題了,下面是有關的方法,以及引用的狀況

AVPacket *av_packet_alloc(void);   						//分配AVPacket
void av_packet_free(AVPacket **pkt); 					//釋放AVPacket
void av_init_packet(AVPacket *pkt);						//初始化AVPacket
int av_new_packet(AVPacket *pkt, int size);				//給AVPacket的buf分配內存,引用計數初始化爲1
int av_packet_ref(AVPacket *dst, const AVPacket *src);	//增長引用計數
void av_packet_unref(AVPacket *pkt);   					//減小引用計數
void av_packet_move_ref(AVPacket *dst, AVPacket *src);	//轉移引用計數
AVPacket *av_packet_clone(const AVPacket *src);	//等於av_packet_alloc()+av_packet_ref()
複製代碼

那麼 咱們要怎麼知道他的引用數呢?有個av_buffer_get_ref_count,能夠獲取Buffer的引用數

在原來的程序基礎上咱們進行修改

void AVPacketMemoryModel::testAVPacketAlloc() {

    AVPacket *packet = av_packet_alloc();
    std::string log = (packet->buf == nullptr) ? "null" : "not null";
    std::cout << log << std::endl;

    av_new_packet(packet, 20 * 1024 * 1024);
    memccpy(packet->data, this, 1, 20 * 1024 * 1024);

    if (packet->buf) {
        int ret = av_buffer_get_ref_count(packet->buf);
        std::cout<<"當前引用值 :"<<ret<<std::endl;
    }

    AVPacket* packet1 = av_packet_alloc();
    av_packet_ref(packet,packet);

    if (packet->buf) {
        int ret = av_buffer_get_ref_count(packet->buf);
        std::cout<<"當前引用值 1 :"<<ret<<std::endl;
    }

}
複製代碼

執行的結果是

AVFrameAVPacket的操做差很少,這裏就不費篇幅敘述了

** 未完持續 。。。**

相關文章
相關標籤/搜索