1. 程式人生 > >[6] ffmpeg + SDL2 實現的視訊播放器「視音訊同步」

[6] ffmpeg + SDL2 實現的視訊播放器「視音訊同步」

日期:2016.10.8
作者:isshe
github:github.com/isshe
郵箱:[email protected]
平臺:ubuntu16.04 64bit

前言

  • 這個程式使用的視音訊同步方法是視訊同步音訊。接下來大概還會學習其他方法,不過下一步應該是先完善功能,實現暫停,播放之類的。
  • 這個版本中是用的是較新的兩個解碼函式avcodec_send_packet(), avcode_receive_frame()。如果舊版本沒有,就換回avcodec_decode_video2()即可。

以下寫著”問題:”的,如果懂請回答,謝謝。

程式結構圖

這裡寫圖片描述

  • 主執行緒開一個執行緒專門負責讀packet放到兩個佇列。
  • 然後主執行緒開音訊裝置。
  • 主執行緒開新執行緒給重新整理函式(refresh_func)
  • 然後主執行緒回去等待事件發生。

同步的思路

視訊同步音訊的方法的實質是:如果視訊慢了就延時短點,如果快了就延時長點。
大體思路是:獲取音訊時間,獲取視訊幀顯示時間,比較兩個時間判斷快了還是慢了,在上一幀的延時上做相應增減。

  • 以音訊為基準的話,如何獲取音訊的當前播放時間(cur_audio_clock)?

    1. 用av_p2d()把時基(time_base)轉換成double型別,再乘以當前packet.pts即可得到當前packet的播放時間(audio_clock)。
    2. 因為一個packet中可能有多幀資料,所以獲取的audio_clock大概只是前面幀的時間戳。
      後面的通過計算獲得:
      • a.計算每秒播放的位元組(bytes)數x,
      • b.獲取當前緩衝區index。
      • c. 用index/x即可得到額外時間。
      • d. 用audio_clock + index/x 即可算出當前播放時間。
      • x的計算方法:樣本率 * 每個樣本大小 * 通道數。
    • 圖示:
    • 這裡寫圖片描述
  • 又如何獲取視訊的當前播放時間(cur_frame_pts)?

    1. 當前packet.pts * time_base + extra_delay
      當前幀的顯示時間戳 * 時基 +額外的延遲時間。
      extra_delay = repeat_pict / (2*fps) (從官網看來的)
  • 如何調整視訊?

    1. 用當前幀pts減去上一幀的pts得到上一幀的延時。
    2. 對比上面獲得的兩個時間(視訊當前時間、音訊當前時間)判斷是快了還是慢了。
      cur_frame_pts - cur_audio_clock > 0 說明快了,小於就慢了。
      設定一個最大容忍值和一個最小容忍值。
      以上一幀延時為基礎,當慢太多就減小延時(delay), 當快太多就加大延時(delay)。
  • 以上就是個人對視訊同步音訊方法的大體理解 , 如有不對請指出,感謝!

主要部分分析

1. 獲取音訊的當前播放時間

  • 獲取當前packet的時間
     if (packet.pts != AV_NOPTS_VALUE)
     {
        ps->audio_clock = packet.pts * av_q2d(ps->paudio_stream->time_base);
     }

1.av_q2d()是Convert rational to double.
AVRational結構有兩個成員:
分子:int nun
分母:int den

  • 獲取當前音訊播放時間
double get_audio_clock(PlayerState *ps)
{
    long long bytes_per_sec = 0;
    double cur_audio_clock = 0.0;
    double cur_buf_pos = ps->audio_buf_index;

    //每個樣本佔2bytes。16bit
    bytes_per_sec = ps->paudio_stream->codec->sample_rate
                * ps->paudio_codec_ctx->channels * 2;

    cur_audio_clock = ps->audio_clock +
                    cur_buf_pos / (double)bytes_per_sec;

    return cur_audio_clock;
}
  1. sample_rate:取樣率(例如44100)
  2. channels:通道數
  3. 2代表2bytes。每個樣本的大小(format指定)
  4. 獲取當前音訊播放時間:audio_clock + packet已經播放的時間.
    另一種獲取當前時間的方法:
    audio_clock+整個packet能播的時間 - 還沒播的時間。 (我參考的程式碼就是這種方法。)

獲取視訊frame的顯示時間

  • 概念:

    • PTS:顯示時間戳。
    • DTS:解碼時間戳。
    • 時基(time_base): 相當於一個單位。(大概是類似與s(秒)之類的)
      • ffmpeg中每個階段有不同是時基。
      • 程式碼中用到兩個來調整時間,分別是:AVStream、AVCodecContext的time_base(都是以秒為單位,轉換的時候要注意,SDL_Delay用的是毫秒, SDL_AddTimer用的是毫秒, av_gettime()用的是微秒)「1s = 1000ms = 1000000us」.
  • 獲取packet.pts並調整

double get_frame_pts(PlayerState *ps, AVFrame *pframe)
{
    double pts = 0.0;
    double frame_delay = 0.0;

    pts = av_frame_get_best_effort_timestamp(pframe);
    if (pts == AV_NOPTS_VALUE)      //???
    {
        pts = 0;
    }

    pts *= av_q2d(ps->pvideo_stream->time_base);

    if (pts != 0)
    {
        ps->video_clock = pts;  
    }
    else
    {
        pts = ps->video_clock;
    }

    //更新video_clock
    //這裡用的是AVCodecContext的time_base
    //extra_delay = repeat_pict / (2*fps), 這個公式是在ffmpeg官網手冊看的
    frame_delay = av_q2d(ps->pvideo_stream->codec->time_base);
    frame_delay += pframe->repeat_pict / (frame_delay * 2);
    ps->video_clock += frame_delay;

    return pts;
}
  1. 以中間的av_q2d為界,av_q2d以及以上程式碼為計算frame的pts。
    當獲取不到的時候,就用av_q2d以下的程式碼來設定。
    問題:什麼時候下會獲取不到呢?

同步調整

  • 同步就是快了就加延時(delay),慢了就減延時(delay)。
  • 下面程式碼中,兩個函式都是自定義的。
    • get_frame_pts():獲取當前幀的顯示時間戳。
    • get_delay():獲取延時時間。注意返回的是一個double型別,返回的時間是 s(秒)。給SDL_Delay()用的時候要*1000。
    • (本來嘗試在get_delay()內部轉換單位,返回一個整型,但是相對比較麻煩,容易出錯。)
     pts = get_frame_pts(ps, pframe);
     //ps中用cur_frame_pts是為了減少get_delay()的引數
     ps->cur_frame_pts = pts; //*(double *)pframe.opaque;
     ps->delay = get_delay(ps) * 1000 + 0.5;
  • get_delay()函式程式碼
double get_delay(PlayerState *ps)
{
    double      ret_delay = 0.0;
    double      frame_delay = 0.0;
    double      cur_audio_clock = 0.0;
    double      compare = 0.0;
    double      threshold = 0.0;

    //這裡的delay是秒為單位, 化為毫秒:*1000
    //獲取兩幀之間的延時
    frame_delay = ps->cur_frame_pts - ps->pre_frame_pts;
    if (frame_delay <= 0 || frame_delay >= 1.0)
    {
        frame_delay = ps->pre_cur_frame_delay;
    }
    //兩幀之間的延時存到統籌結構
    ps->pre_cur_frame_delay = frame_delay;
    ps->pre_frame_pts = ps->cur_frame_pts;

    cur_audio_clock = get_audio_clock(ps);

    //compare < 0 說明慢了, > 0說明快了
    compare = ps->cur_frame_pts - cur_audio_clock;

    //設定閥值, 是一個正數,最小閥值取它的負數。
    //這裡設閥值為兩幀之間的延遲,
    threshold = frame_delay;
    //SYNC_THRESHOLD ? frame_delay : SYNC_THRESHOLD;


    if (compare <= -threshold)      //慢, 加快速度, 
    {
        ret_delay = frame_delay / 2;  //這裡用移位更好快
    }
    else if (compare >= threshold)  //快了,就在上一幀延時的基礎上加長延時
    {
        ret_delay = frame_delay * 2; //這裡用移位更好快
    }
    else
    {
        ret_delay = frame_delay;
    }

    return ret_delay;
}

所遇到的問題

1. ffmpeg共享記憶體導致的錯誤

  1. 本來的實現是用一個新執行緒負責解碼(packet->frame), 解碼後把frame放入佇列,像把packet放入佇列那樣。
    這裡會有一個畫面閃爍或者嚴重丟幀的問題。
    原因就是每次新建的frame入隊的時候,會共享記憶體, 這一個frame覆蓋前一個frame。
    • 解決辦法有:
      a. 把佇列長度設為1。每次只解碼一幀到佇列,取了以後再解。
      b. 最容易想到的肯定是找不共享記憶體的方法了.
    • 嘗試過不共享記憶體的方法有:
      av_frame_clone(),
      av_frame_alloc()以後av_frame_ref()「這個看了手冊,但不理解,只是用了」
      av_malloc()後用memcpy()
      av_frame_alloc後用memcpy().
      都失敗了。然後用a的方法實現了同步後,重寫這部分程式碼,把frame佇列刪了。

參考資料

程式碼下載

  • 接觸ffmpeg已經8天了,搭環境用了兩天,因為不知道怎麼才算對,搭好了編譯下載的程式不過,又再搭如此反覆。最後還是看雷神的視訊才懂,真的感謝他。
  • 學習這幾個程式,一次次重寫,學習到了統籌結構很重要,也很好用
  • 還學習到,當出現錯誤的時候,
    • 如果是有不懂的函式,結構,要去查詢,理解再繼續。
    • 如果函式之類都沒問題,是邏輯之類問題,就認真看程式,去想,思考。
    • 而不要盲目地一個個去試錯。