1. 程式人生 > 實用技巧 >ffplay原始碼分析07 ---- 音視訊同步

ffplay原始碼分析07 ---- 音視訊同步

⾳視訊同步策略

  1. 以⾳頻為基準,同步視訊到⾳頻(AV_SYNC_AUDIO_MASTER
    • 視訊慢了則丟掉部分視訊幀(視覺->畫⾯跳幀)
    • 視訊快了則繼續渲染上⼀幀 
  2. 以視訊為基準,同步⾳頻到視訊(AV_SYNC_VIDEO_MASTER
    • ⾳頻慢了則加快播放速度(或丟掉部分⾳頻幀,丟幀極容易聽出來斷⾳)
    • ⾳頻快了則放慢播放速度(或重複上⼀幀 )
    • ⾳頻改變播放速度時涉及到重取樣
  3. 以外部時鐘為基準,同步⾳頻和視訊到外部時鐘(AV_SYNC_EXTERNAL_CLOCK
    • 前兩者的綜合,根據外部時鐘改變播放速度
  4. 視訊和⾳頻各⾃輸出,即不作同步處理(FREE RUN

一般是第一種,就是將視訊同步到音訊。

音視訊同步基本概念

  • DTS(Decoding Time Stamp):即解碼時間戳,這個時間戳的意義在於告訴播放器該在什麼時候解碼這⼀幀的資料。
  • PTS(Presentation Time Stamp):即顯示時間戳,這個時間戳⽤來告訴播放器該在什麼時候顯示這⼀幀的資料。
  • timebase 時基:pts的值的真正單位
  • ffplay中的pts,ffplay在做⾳視訊同步時使⽤秒為單位,使⽤double型別去標識pts,在ffmpeg內部不會⽤浮點數去標記pts。
  • Clock 時鐘

當視訊流中沒有 B 幀時,通常 DTS 和 PTS 的順序是⼀致的。但存在B幀的時候兩者的順序就不⼀致了。

1.

pts是presentation timestamp的縮寫,即顯示時間戳,⽤於標記⼀個幀的呈現時刻,它的單位由timebase決定。timebase的型別是結構體AVRational(⽤於表示分數):

typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

如timebase={1, 1000} 表示千分之⼀秒(毫秒),那麼pts=1000,即為pts*1/1000 = 1秒,那麼這⼀幀就需要在第⼀秒的時候呈現。

將AVRatioal結構轉換成double:

static
inline double av_q2d(AVRational a){ return a.num / (double) a.den; }

計算時間戳:

timestamp(秒) = pts * av_q2d(st->time_base)

計算幀時長:

time(秒) = st->duration * av_q2d(st->time_base)

不同時間基之間的轉換:

int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

在ffplay中,將pts轉化為秒,⼀般做法是: pts * av_q2d(timebase)

2. 在做同步的時候,我們需要⼀個"時鐘"的概念,⾳頻、視訊、外部時鐘都有⾃⼰獨⽴的時鐘,各⾃set各⾃的時鐘,以誰為基準(master), 其他的則只能get該時鐘進⾏同步,ffplay定義的結構體是Clock:

// 這裡講的系統時鐘 是通過av_gettime_relative()獲取到的時鐘,單位為微妙
typedef struct Clock {
    double    pts;            // 時鐘基礎, 當前幀(待播放)顯示時間戳,播放後,當前幀變成上一幀
    // 當前pts與當前系統時鐘的差值, audio、video對於該值是獨立的
    double    pts_drift;      // clock base minus time at which we updated the clock
    // 當前時鐘(如視訊時鐘)最後一次更新時間,也可稱當前時鐘時間
    double    last_updated;   // 最後一次更新的系統時鐘
    double    speed;          // 時鐘速度控制,用於控制播放速度
    // 播放序列,所謂播放序列就是一段連續的播放動作,一個seek操作會啟動一段新的播放序列
    int    serial;             // clock is based on a packet with this serial
    int    paused;             // = 1 說明是暫停狀態
    // 指向packet_serial
    int *queue_serial;      /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

這個時鐘的⼯作原理是這樣的:
1. 需要不斷“對時”。對時的⽅法set_clock_at(Clock *c, double pts, int serial,double time) ,需要⽤pts、serial、time(系統時間)進⾏對時。

2. 獲取的時間是⼀個估算值。估算是通過對時時記錄的pts_drift估算的。pts_drift是最精華的設計,⼀定要理解。

可以看這個圖來幫助理解:

圖中央是⼀個時間軸(time是⼀直在按時間遞增),從左往右看。⾸先我們調⽤set_clock 進⾏⼀次對時,假設這時的pts 是落後時間time 的,那麼計算pts_drift = pts - time ,計算出pts和time的相對差值。這個可以理解為pts到time的距離,所以pts = time +pts_drift。pts是隨著時間增加的,系統時間也在同步增加,這個距離pts_drift是不會變的。

接著,過了⼀會⼉,且在下次對時前,通過get_clock 來查詢時間,因為set_clock時的pts 已經過時,不能直接拿set_clock時的pts當做這個時鐘的時間。不過我們前⾯計算過pts_drift ,所以我們可以通過當前時刻的時間來估算當前時刻的pts: pts = time +pts_drift 。

FFmpeg中的時間單位

AV_TIME_BASE

  • 定義#define AV_TIME_BASE 1 000 000
  • ffmpeg中的內部計時單位(時間基)

AV_TIME_BASE_Q

  • 定義#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
  • ffmpeg內部時間基的分數表示,實際上它是AV_TIME_BASE的倒數

時間基轉換公式

  • timestamp(ffmpeg內部時間戳) = AV_TIME_BASE * time(秒)
  • time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg內部時間戳)