C語言將音視頻時鐘同步封裝成通用模塊的方法
前言
編寫視頻播放器時需要實現(xiàn)音視頻的時鐘同步,這個功能是不太容易實現(xiàn)的。雖然理論通常是知道的,但是不同過實際的調試很難寫出可用的時鐘同步功能。其實也是有可以參考的代碼,ffplay中實現(xiàn)了3種同步,但實現(xiàn)邏輯較為復雜,比較難直接提取出來使用。筆者通過參考ffplay在自己的播放器中實現(xiàn)了時鐘同步,參考。在實現(xiàn)過程中發(fā)現(xiàn)此功能可以做成一個通用的模塊,在任何音視頻播放的場景都可以使用。
一、視頻時鐘
1、時鐘計算方法
視頻的時鐘基于視頻幀的時間戳,由于視頻是通過一定的幀率渲染的,采用直接讀取當前時間戳的方式獲取時鐘會造成一定的誤差,精度不足。我們要獲取準確連續(xù)的時間,應該使用一個系統(tǒng)的時鐘,視頻更新時記錄時鐘的起點,用系統(tǒng)時鐘偏移后作為視頻時鐘,這樣才能得到足夠精度的時鐘。流程如下:
每次渲染視頻幀時,更新視頻時鐘起點
視頻時鐘起點 = 系統(tǒng)時鐘 − 視頻時間戳 視頻時鐘起點=系統(tǒng)時鐘-視頻時間戳 視頻時鐘起點=系統(tǒng)時鐘−視頻時間戳
任意時刻獲取視頻時鐘
視頻時鐘 = 系統(tǒng)時鐘 − 視頻時鐘起點 視頻時鐘=系統(tǒng)時鐘-視頻時鐘起點 視頻時鐘=系統(tǒng)時鐘−視頻時鐘起點
代碼示例如下:
定義相關變量
//視頻時鐘起點,單位秒 double videoStartTime=0;
更新視頻時鐘起點
//更新視頻時鐘(或者說矯正更準確)起點,pts為當前視頻幀的時間戳,getCurrentTime為獲取當前系統(tǒng)時鐘的時間,單位秒 videoStartTime=getCurrentTime()-pts;
獲取視頻時鐘
//獲取視頻時鐘,單位秒 double videoTime=getCurrentTime()-videoStartTime;
2、同步視頻時鐘
有了上述時鐘的計算方法,我們可以獲得一個準確的視頻時鐘。為了確保視頻能夠在正確的時間渲染我們還需要進行視頻渲染時的時鐘同步。
同步流程如下所示,流程圖中的“更新視頻時鐘起點”和“獲取視頻時鐘”與一節(jié)的計算方法直接對應。
核心代碼示例如下:
/// <summary> /// 同步視頻時鐘 /// 視頻幀顯示前調用此方法,傳入幀的pts和duration,根據(jù)此方法的返回值確定顯示還是丟幀或者延時。 /// </summary> /// <param name="pts">視頻幀pts,單位為s</param> /// <param name="duration">視頻幀時長,單位為s。</param> /// <returns>大于0則延時,值為延時時長,單位s。等于0顯示。小于0丟幀</returns> double synVideo(double pts, double duration) { if (videoStartTime== 0) //初始化視頻起點 { videoStartTime= getCurrentTime() - pts; } //以下變量時間單位為s //獲取視頻時鐘 double currentTime = getCurrentTime() - videoStartTime; //計算時間差,大于0則late,小于0則early。 double diff = currentTime - pts; //時間早了延時 if (diff < -0.001) { if (diff < -0.1) { diff = -0.1; } return -diff; } //時間晚了丟幀,duration為一幀的持續(xù)時間,在一個duration內是正常時間,加一個duration作為閾值來判斷丟幀。 if (diff > 2 * duration) { return -1; } //更新視頻時鐘起點 videoStartTime= getCurrentTime() - pts; return 0; }
3、同步到另一個時鐘
我實現(xiàn)了視頻時鐘的同步,但有時還需要視頻同步到其他時鐘,比如同步到音頻時鐘或外部時鐘。將視頻時鐘同步到另外一個時鐘很簡單,在計算出視頻時鐘偏差diff后再加上視頻時鐘與另外一個時鐘的差值就可以了。
上一節(jié)的流程圖基礎上添加如下加粗的步驟
上一節(jié)的代碼加入如下內容
//計算時間差,大于0則late,小于0則early。 double diff = currentTime - pts; //同步到另一個時鐘-start double sDiff = 0; //anotherStartTime為另一個時鐘的起始時間 sDiff = videoStartTime - anotherStartTime; diff += sDiff; //同步到另一個時鐘-end //時間早了延時 if (diff < -0.001)
二、音頻時鐘
1、時鐘計算方法
音頻時鐘的計算和視頻時鐘有點不一樣,但結構上是差不多的,只是音頻是通過通過數(shù)據(jù)長度來計算時間的。
(1)、時間公式 公式一
時長 = 音頻數(shù)據(jù)長度( b y t e s ) / ( 聲道數(shù) ∗ 采樣率 ∗ 位深度 / 8 ) 時長=音頻數(shù)據(jù)長度(bytes)/(聲道數(shù)*采樣率*位深度/8) 時長=音頻數(shù)據(jù)長度(bytes)/(聲道數(shù)∗采樣率∗位深度/8)
代碼如下:
//聲道數(shù) int channels=2; //采樣率 int samplerate=48000; //位深 int bitsPerSample=32; //數(shù)據(jù)長度 int dataSize=8192; //時長,單位秒 double duration=(double)dataSize/(channels*samplerate*bitsPerSample/8); //duration 值為:0.0426666...
公式二
時長 = 采樣數(shù) / 采樣率
時長=采樣數(shù)/采樣率 時長=采樣數(shù)/采樣率
代碼如下:
//采樣數(shù) int samples=1024; //采樣率 int samplerate=48000; //時長,單位秒 double duration=(double)samples/samplerate; //duration值為0.021333333...
(2)、計算方法
計算音頻時鐘首先需要記錄音頻的時間戳,計算音頻時間戳需要將每次播放的音頻數(shù)據(jù)轉換成時長累加起來如下:(其中n表示累計的播放次數(shù))
音頻時間戳 = ∑ i = 0 n 音頻數(shù)據(jù)長度( b y t e s ) i / ( 聲道數(shù) ∗ 采樣率 ∗ 位深度 / 8 ) 音頻時間戳=\sum_{i=0}^n 音頻數(shù)據(jù)長度(bytes)i/(聲道數(shù)*采樣率*位深度/8) 音頻時間戳=i=0∑n?音頻數(shù)據(jù)長度(bytes)i/(聲道數(shù)∗采樣率∗位深度/8)
或者
音頻時間戳 = ∑ i = 0 n 采樣數(shù) i / 采樣率 音頻時間戳=\sum_{i=0}^n 采樣數(shù)i/采樣率 音頻時間戳=i=0∑n?采樣數(shù)i/采樣率
有了音頻時間戳就可可以計算出音頻時鐘的起點
音頻時鐘起點 = 系統(tǒng)時鐘 − 音頻時間戳 音頻時鐘起點=系統(tǒng)時鐘-音頻時間戳 音頻時鐘起點=系統(tǒng)時鐘−音頻時間戳
通過音頻時鐘起點就可以計算音頻時鐘了
音頻時鐘 = 系統(tǒng)時鐘 − 音頻時鐘起點 音頻時鐘=系統(tǒng)時鐘-音頻時鐘起點 音頻時鐘=系統(tǒng)時鐘−音頻時鐘起點
代碼示例:
定義相關變量
//音頻時鐘起點,單位秒 double audioStartTime=0; //音頻時間戳,單位秒 double currentPts=0;
更新音頻時鐘(通過采樣數(shù))
//計算時間戳,samples為當前播放的采樣數(shù) currentPts += (double)samples / samplerate; //計算音頻起始時間 audioStartTime= getCurrentTime() - currentPts;
更新音頻時鐘(通過數(shù)據(jù)長度)
currentPts += (double)bytesSize / (channels *samplerate *bitsPerSample/8); //計算音頻起始時間 audioStartTime= getCurrentTime() - currentPts;
獲取音頻時鐘
//獲取音頻時鐘,單位秒 double audioTime= getCurrentTime() - audioStartTime;
2、同步音頻時鐘
有了時間戳的計算方法,接下來就需要確定同步的時機,以確保計算的時鐘是準確的。通常按照音頻設備播放音頻的耗時去更新音頻時鐘就是準確的。
我們根據(jù)上述計算過方法封裝一個更新音頻時鐘的方法:
/// <summary> /// 更新音頻時鐘 /// </summary> /// <param name="samples">采樣數(shù)</param> /// <param name="samplerate">采樣率</param> /// <returns>應該播放的采樣數(shù)</returns> int synAudio(int samples, int samplerate) { currentPts += (double)samples / samplerate; //getCurrentTime為獲取當前系統(tǒng)時鐘的時間 audioStartTime= getCurrentTime() - currentPts; return samples; } /// <summary> /// 更新音頻時鐘,通過數(shù)據(jù)長度 /// </summary> /// <param name="bytesSize">數(shù)據(jù)長度</param> /// <param name="samplerate">采樣率</param> /// <param name="channels">聲道數(shù)</param> /// <param name="bitsPerSample">位深</param> /// <returns>應該播放的采樣數(shù)</returns> int synAudioByBytesSize(size_t bytesSize, int samplerate, int channels, int bitsPerSample) { return synAudio((double)bytesSize / (channels * bitsPerSample/8), samplerate) * (bitsPerSample/8)* channels; }
(1)、阻塞式
播放音頻的時候可以使用阻塞的方式,輸入音頻數(shù)據(jù)等待設備播放完成再返回,這個等待時間可以真實的反映音頻設備播放音頻數(shù)據(jù)的耗時,我們播放完成后更新音頻時鐘即可。
(2)、回調式
回調式的寫入音頻數(shù)據(jù),比如sdl就有這種方式。我們在回調中更新音頻時鐘。
3、同步到另一個時鐘
音頻通常情況是不需要同步到另一個時鐘的,因為音頻的播放本身就不需要時鐘校準。但是還是有一些場景需要音頻同步到另一個時鐘比如多軌道播放。
關鍵流程如下所示:
計算應寫入數(shù)據(jù)長度的代碼示例:(其中samples為音頻設備需要寫入的采樣數(shù))
//獲取音頻時鐘當前時間 double audioTime = getCurrentTime() - audioStartTime; double diff = 0; //計算差值,getMasterTime獲取另一個時鐘的當前時間 diff = getMasterTime(syn) - audioTime; int oldSamples = samples; if (fabs(diff) > 0.01) //超過偏差閾值,加上差值對應的采樣數(shù) { samples += diff * samplerate; } if (samples < 0) //最小以不播放等待 { samples = 0; } if (samples > oldSamples * 2) //最大以2倍速追趕 { samples = oldSamples * 2; } //輸出samples為寫入數(shù)據(jù)長度,后面音頻時鐘計算也以此samples為準。
三、外部時鐘
播放視頻的時候還可以參照一個外部的時鐘,視頻和音頻都向外部時鐘去同步。
1、絕對時鐘
播放視頻或者音頻的時候,偶爾因為外部原因,比如系統(tǒng)卡頓、網(wǎng)絡變慢、磁盤讀寫變慢會導致播放時間延遲了。如果一個1分鐘的視頻,播放過程中卡頓了幾秒,那最終會在1分鐘零幾秒后才能播完視頻。如果我們一定要在1分鐘將視頻播放完成,那就可以使用絕對時鐘作為外部時鐘。
本文采用的就是這種時鐘,大致實現(xiàn)步驟如下:
視頻開始播放-->設置絕對時鐘起點-->視頻同步到絕對時鐘-->音頻同步到絕對時鐘
四、封裝成通用模塊
1、完整代碼
/************************************************************************ * @Project: Synchronize * @Decription: 視頻時鐘同步模塊 * 這是一個通用的視頻時鐘同步模塊,支持3種同步方式:同步到視頻時鐘、同步到音頻時鐘、以及同步到外部時鐘(絕對時鐘)。 * 使用方法也比較簡單,初始化之后在視頻顯示和音頻播放處調用相應方法即可。 * 非線程安全:內部實現(xiàn)未使用線程安全機制。對于單純的同步一般不用線程安全機制。當需要定位時可能需要一定的互斥操作。 * 沒有特殊依賴(目前版本依賴了ffmpeg的獲取系統(tǒng)時鐘功能,如果換成c++則一行代碼可以實現(xiàn),否則需要宏區(qū)分實現(xiàn)各個平臺的系統(tǒng)時鐘獲?。? * @Verision: v1.0 * @Author: Xin Nie * @Create: 2022/9/03 14:55:00 * @LastUpdate: 2022/9/22 16:36:00 ************************************************************************ * Copyright @ 2022. All rights reserved. ************************************************************************/ /// <summary> /// 時鐘對象 /// </summary> typedef struct { //起始時間 double startTime; //當前pts double currentPts; }Clock; /// <summary> /// 時鐘同步類型 /// </summary> typedef enum { //同步到音頻 SYNCHRONIZETYPE_AUDIO, //同步到視頻 SYNCHRONIZETYPE_VIDEO, //同步到絕對時鐘 SYNCHRONIZETYPE_ABSOLUTE }SynchronizeType; /// <summary> /// 時鐘同步對象 /// </summary> typedef struct { /// <summary> /// 音頻時鐘 /// </summary> Clock audio; /// <summary> /// 視頻時鐘 /// </summary> Clock video; /// <summary> /// 絕對時鐘 /// </summary> Clock absolute; /// <summary> /// 時鐘同步類型 /// </summary> SynchronizeType type; /// <summary> /// 估算的視頻幀時長 /// </summary> double estimateVideoDuration; /// <summary> /// 估算視頻幀數(shù) /// </summary> double n; }Synchronize; /// <summary> /// 返回當前時間 /// </summary> /// <returns>當前時間,單位秒,精度微秒</returns> static double getCurrentTime() { //此處用的是ffmpeg的av_gettime_relative。如果沒有ffmpeg環(huán)境,則可替換成平臺獲取時鐘的方法:單位為秒,精度需要微妙,相對絕對時鐘都可以。 return av_gettime_relative() / 1000000.0; } /// <summary> /// 重置時鐘同步 /// 通常用于暫停、定位 /// </summary> /// <param name="syn">時鐘同步對象</param> void synchronize_reset(Synchronize* syn) { SynchronizeType type = syn->type; memset(syn, 0, sizeof(Synchronize)); syn->type = type; } /// <summary> /// 獲取主時鐘 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <returns>主時鐘對象</returns> Clock* synchronize_getMasterClock(Synchronize* syn) { switch (syn->type) { case SYNCHRONIZETYPE_AUDIO: return &syn->audio; case SYNCHRONIZETYPE_VIDEO: return &syn->video; case SYNCHRONIZETYPE_ABSOLUTE: return &syn->absolute; default: break; } return 0; } /// <summary> /// 獲取主時鐘的時間 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <returns>時間,單位s</returns> double synchronize_getMasterTime(Synchronize* syn) { return getCurrentTime() - synchronize_getMasterClock(syn)->startTime; } /// <summary> /// 設置時鐘的時間 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <param name="pts">當前時間,單位s</param> void synchronize_setClockTime(Synchronize* syn, Clock* clock, double pts) { clock->currentPts = pts; clock->startTime = getCurrentTime() - pts; } /// <summary> /// 獲取時鐘的時間 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <param name="clock">時鐘對象</param> /// <returns>時間,單位s</returns> double synchronize_getClockTime(Synchronize* syn, Clock* clock) { return getCurrentTime() - clock->startTime; } /// <summary> /// 更新視頻時鐘 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <param name="pts">視頻幀pts,單位為s</param> /// <param name="duration">視頻幀時長,單位為s。缺省值為0,內部自動估算duration</param> /// <returns>大于0則延時值為延時時長,等于0顯示,小于0丟幀</returns> double synchronize_updateVideo(Synchronize* syn, double pts, double duration) { if (duration == 0) //估算duration { if (pts != syn->video.currentPts) syn->estimateVideoDuration = (syn->estimateVideoDuration * syn->n + pts - syn->video.currentPts) / (double)(syn->n + 1); duration = syn->estimateVideoDuration; //只估算最新3幀 if (syn->n++ > 3) syn->estimateVideoDuration = syn->n = 0; if (duration == 0) duration = 0.1; } if (syn->video.startTime == 0) { syn->video.startTime = getCurrentTime() - pts; } //以下變量時間單位為s //當前時間 double currentTime = getCurrentTime() - syn->video.startTime; //計算時間差,大于0則late,小于0則early。 double diff = currentTime - pts; double sDiff = 0; if (syn->type != SYNCHRONIZETYPE_VIDEO && synchronize_getMasterClock(syn)->startTime != 0) //同步到主時鐘 { sDiff = syn->video.startTime - synchronize_getMasterClock(syn)->startTime; diff += sDiff; } //時間早了延時 if (diff < -0.001) { if (diff < -0.1) { diff = -0.1; } return -diff; } syn->video.currentPts = pts; //時間晚了丟幀,duration為一幀的持續(xù)時間,在一個duration內是正常時間,加一個duration作為閾值來判斷丟幀。 if (diff > 2 * duration) { return -1; } //更新視頻時鐘 printf("video-time:%.3lfs audio-time:%.3lfs absolute-time:%.3lfs synDiff:%.4lfms diff:%.4lfms \r", pts, getCurrentTime() - syn->audio.startTime, getCurrentTime() - syn->absolute.startTime, sDiff * 1000, diff * 1000); syn->video.startTime = getCurrentTime() - pts; if (syn->absolute.startTime == 0) //初始化絕對時鐘 { syn->absolute.startTime = syn->video.startTime; } return 0; } /// <summary> /// 更新音頻時鐘 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <param name="samples">采樣數(shù)</param> /// <param name="samplerate">采樣率</param> /// <returns>應該播放的采樣數(shù)</returns> int synchronize_updateAudio(Synchronize* syn, int samples, int samplerate) { if (syn->type != SYNCHRONIZETYPE_AUDIO && synchronize_getMasterClock(syn)->startTime != 0) { //同步到主時鐘 double audioTime = getCurrentTime() - syn->audio.startTime; double diff = 0; diff = synchronize_getMasterTime(syn) - audioTime; int oldSamples = samples; if (fabs(diff) > 0.01) { samples += diff * samplerate; } if (samples < 0) { samples = 0; } if (samples > oldSamples * 2) { samples = oldSamples * 2; } } syn->audio.currentPts += (double)samples / samplerate; syn->audio.startTime = getCurrentTime() - syn->audio.currentPts; if (syn->absolute.startTime == 0) //初始化絕對時鐘 { syn->absolute.startTime = syn->audio.startTime; } return samples; } /// <summary> /// 更新音頻時鐘,通過數(shù)據(jù)長度 /// </summary> /// <param name="syn">時鐘同步對象</param> /// <param name="bytesSize">數(shù)據(jù)長度</param> /// <param name="samplerate">采樣率</param> /// <param name="channels">聲道數(shù)</param> /// <param name="bitsPerSample">位深</param> /// <returns>應該播放的數(shù)據(jù)長度</returns> int synchronize_updateAudioByBytesSize(Synchronize* syn, size_t bytesSize, int samplerate, int channels, int bitsPerSample) { return synchronize_updateAudio(syn, bytesSize / (channels * bitsPerSample/8), samplerate) * (bitsPerSample /8)* channels; }
五、使用示例
1、基本用法
(1)、初始化
Synchronize syn; memset(&syn,0,sizeof(Synchronize));
(2)、設置同步類型
設置同步類型,默認不設置則為同步到音頻
//同步到音頻 syn->type=SYNCHRONIZETYPE_AUDIO; //同步到視頻 syn->type=SYNCHRONIZETYPE_VIDEO; //同步到絕對時鐘 syn->type=SYNCHRONIZETYPE_ABSOLUTE;
(3)、視頻同步
在視頻渲染處調用,如果只有視頻沒有音頻,需要注意將同步類型設置為SYNCHRONIZETYPE_VIDEO、或SYNCHRONIZETYPE_ABSOLUTE。
//當前幀的pts,單位s double pts; //當前幀的duration,單位s double duration; //視頻同步 double delay =synchronize_updateVideo(&syn,pts,duration); if (delay > 0) //延時 { //延時delay時長,單位s } else if (delay < 0) //丟幀 { } else //播放 { }
(4)、音頻同步
在音頻播放處調用
//將要寫入的采樣數(shù) int samples; //音頻的采樣率 int samplerate; //時鐘同步,返回的samples為實際寫入的采樣數(shù),將要寫入的采樣數(shù)不能變,實際采樣數(shù)需要壓縮或拓展到將要寫入的采樣數(shù)。 samples = synchronize_updateAudio(&syn, samples, samplerate);
(5)、獲取播放時間
獲取當前播放時間
//返回當前時鐘,單位s。 double cursorTime=synchronize_getMasterTime(&syn);
(6)、暫停
暫停后直接將時鐘重置即可。但在重新開始播放之前將無法獲取正確的播放時間。
void pause(){ //暫停邏輯 ... //重置時鐘 synchronize_reset(&syn); }
(7)、定位
定位后直接將時鐘重置即可,需要注意多線程情況下避免重置時鐘后又被更新。
void seek(double pts){ //定位邏輯 ... //重置時鐘 synchronize_reset(&syn); }
在音頻解碼或即將播放處,校正音頻定位后的時間戳。
//音頻定位后第一幀的時間戳。 double pts; synchronize_setClockTime(&syn, &syn.audio, pts);
總結
以上就是今天要講的內容,本文簡單介紹了音視頻的時鐘同步的原理以及具體實現(xiàn),其中大部分原理參考了ffplay,做了一定的簡化。本文提供的只是其中一種音視頻同步方法,其他方法比如視頻的同步可以直接替換時鐘,視頻直接參照給定的時鐘去做同步也是可以的。音頻時鐘的同步策略比如采樣數(shù)的計算也可以根據(jù)具體情況做調整??偟膩碚f,這是一個通用的音視頻同步模塊,能夠適用于一般的視頻播放需求,可以很大程度的簡化實現(xiàn)。
附錄 1、獲取系統(tǒng)時鐘
由于完整代碼的獲取系統(tǒng)時鐘的方法依賴于ffmpeg環(huán)境,考慮到不需要ffmpeg的情況下需要自己實現(xiàn),這里貼出一些平臺獲取系統(tǒng)時鐘的方法
(1)、Windows
#include<Windows.h> /// <summary> /// 返回當前時間 /// </summary> /// <returns>當前時間,單位秒,精度微秒</returns> static double getCurrentTime() { LARGE_INTEGER ticks, Frequency; QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&ticks); return (double)ticks.QuadPart / (double)Frequency.QuadPart; }
(2)、C++11
#include<chrono> /// <summary> /// 返回當前時間 /// </summary> /// <returns>當前時間,單位秒,精度微秒</returns> static double getCurrentTime() { return std::chrono::time_point_cast <std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now()).time_since_epoch().count() / 1e+9; }
到此這篇關于c語言將音視頻時鐘同步封裝成通用模塊的文章就介紹到這了,更多相關c語言音視頻時鐘內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
ubuntu系統(tǒng)下C++調用matlab程序的方法詳解
學習c++與matlab混合編程一般是通過c++調用matlab函數(shù),因為matlab具有強大的數(shù)學函數(shù)庫,然而vc++具有界面設計靈活的優(yōu)點,下面這篇文章主要給大家介紹了關于在ubuntu系統(tǒng)下C++調用matlab程序的方法,需要的朋友可以參考下。2017-08-08C++實現(xiàn)LeetCode(199.二叉樹的右側視圖)
這篇文章主要介紹了C++實現(xiàn)LeetCode(199.二叉樹的右側視圖),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下2021-08-08