
該文章首發於微信公眾號:字節流動
FFmpeg 開發系列連載:
FFmpeg 開發(01):FFmpeg 編譯和集成
FFmpeg 開發(02):FFmpeg + ANativeWindow 實現視頻解碼播放
FFmpeg 開發(03):FFmpeg + OpenSLES 實現音頻解碼播放
FFmpeg 開發(04):FFmpeg + OpenGLES 實現音頻可視化播放
FFmpeg 開發(05):FFmpeg + OpenGLES 實現視頻解碼播放和視頻濾鏡
前文中,我們基於 FFmpeg 利用 OpenGL ES 和 OpenSL ES 分別實現了對解碼后視頻和音頻的渲染,本文將實現播放器的最后一個重要功能:音視頻同步。
老人們經常說,播放器對音頻和視頻的播放沒有絕對的靜態的同步,只有相對的動態的同步,實際上音視頻同步就是一個“你追我趕”的過程。
音視頻的同步方式有 3 種,即:音視頻向系統時鍾同步、音頻向視頻同步及視頻向音頻同步。
音視頻解碼器結構
在實現音視頻同步之前,我們先簡單說下本文播放器的大致結構,方便后面實現不同的音視頻同步方式。

如上圖所示,音頻解碼和視頻解碼分別占用一個獨立線程,線程里有一個解碼循環,解碼循環里不斷對音視頻編碼數據進行解碼,音視頻解碼幀不設置緩存 Buffer , 進行實時渲染,極大地方便了音視頻同步的實現。
音視頻解碼線程獨立分離的播放器模式,簡單靈活,代碼量小,面向初學者,可以很方便實現音視頻同步。
音視和視頻解碼流程非常相似,所以我們可以將二者的解碼器抽象為一個基類:
class DecoderBase : public Decoder {
public:
DecoderBase()
{};
virtual~ DecoderBase()
{};
//開始播放
virtual void Start();
//暫停播放
virtual void Pause();
//停止
virtual void Stop();
//獲取時長
virtual float GetDuration()
{
//ms to s
return m_Duration * 1.0f / 1000;
}
//seek 到某個時間點播放
virtual void SeekToPosition(float position);
//當前播放的位置,用於更新進度條和音視頻同步
virtual float GetCurrentPosition();
virtual void ClearCache()
{};
virtual void SetMessageCallback(void* context, MessageCallback callback)
{
m_MsgContext = context;
m_MsgCallback = callback;
}
//設置音視頻同步的回調
virtual void SetAVSyncCallback(void* context, AVSyncCallback callback)
{
m_AVDecoderContext = context;
m_AudioSyncCallback = callback;
}
protected:
void * m_MsgContext = nullptr;
MessageCallback m_MsgCallback = nullptr;
virtual int Init(const char *url, AVMediaType mediaType);
virtual void UnInit();
virtual void OnDecoderReady() = 0;
virtual void OnDecoderDone() = 0;
//解碼數據的回調
virtual void OnFrameAvailable(AVFrame *frame) = 0;
AVCodecContext *GetCodecContext() {
return m_AVCodecContext;
}
private:
int InitFFDecoder();
void UnInitDecoder();
//啟動解碼線程
void StartDecodingThread();
//音視頻解碼循環
void DecodingLoop();
//更新顯示時間戳
void UpdateTimeStamp();
//音視頻同步
void AVSync();
//解碼一個packet編碼數據
int DecodeOnePacket();
//線程函數
static void DoAVDecoding(DecoderBase *decoder);
//封裝格式上下文
AVFormatContext *m_AVFormatContext = nullptr;
//解碼器上下文
AVCodecContext *m_AVCodecContext = nullptr;
//解碼器
AVCodec *m_AVCodec = nullptr;
//編碼的數據包
AVPacket *m_Packet = nullptr;
//解碼的幀
AVFrame *m_Frame = nullptr;
//數據流的類型
AVMediaType m_MediaType = AVMEDIA_TYPE_UNKNOWN;
//文件地址
char m_Url[MAX_PATH] = {0};
//當前播放時間
long m_CurTimeStamp = 0;
//播放的起始時間
long m_StartTimeStamp = -1;
//總時長 ms
long m_Duration = 0;
//數據流索引
int m_StreamIndex = -1;
//鎖和條件變量
mutex m_Mutex;
condition_variable m_Cond;
thread *m_Thread = nullptr;
//seek position
volatile float m_SeekPosition = 0;
volatile bool m_SeekSuccess = false;
//解碼器狀態
volatile int m_DecoderState = STATE_UNKNOWN;
void* m_AVDecoderContext = nullptr;
AVSyncCallback m_AudioSyncCallback = nullptr;//用作音視頻同步
};
篇幅有限,代碼貼多了容易導致視覺疲勞,完整實現代碼見閱讀原文,這里只貼出幾個關鍵函數。
解碼循環。
void DecoderBase::DecodingLoop() {
LOGCATE("DecoderBase::DecodingLoop start, m_MediaType=%d", m_MediaType);
{
std::unique_lock<std::mutex> lock(m_Mutex);
m_DecoderState = STATE_DECODING;
lock.unlock();
}
for(;;) {
while (m_DecoderState == STATE_PAUSE) {
std::unique_lock<std::mutex> lock(m_Mutex);
LOGCATE("DecoderBase::DecodingLoop waiting, m_MediaType=%d", m_MediaType);
m_Cond.wait_for(lock, std::chrono::milliseconds(10));
m_StartTimeStamp = GetSysCurrentTime() - m_CurTimeStamp;
}
if(m_DecoderState == STATE_STOP) {
break;
}
if(m_StartTimeStamp == -1)
m_StartTimeStamp = GetSysCurrentTime();
if(DecodeOnePacket() != 0) {
//解碼結束,暫停解碼器
std::unique_lock<std::mutex> lock(m_Mutex);
m_DecoderState = STATE_PAUSE;
}
}
LOGCATE("DecoderBase::DecodingLoop end");
}
獲取當前時間戳。
void DecoderBase::UpdateTimeStamp() {
LOGCATE("DecoderBase::UpdateTimeStamp");
//參照 ffplay
std::unique_lock<std::mutex> lock(m_Mutex);
if(m_Frame->pkt_dts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pkt_dts;
} else if (m_Frame->pts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pts;
} else {
m_CurTimeStamp = 0;
}
m_CurTimeStamp = (int64_t)((m_CurTimeStamp * av_q2d(m_AVFormatContext->streams[m_StreamIndex]->time_base)) * 1000);
}
解碼一個 packet 的編碼數據。
int DecoderBase::DecodeOnePacket() {
int result = av_read_frame(m_AVFormatContext, m_Packet);
while(result == 0) {
if(m_Packet->stream_index == m_StreamIndex) {
if(avcodec_send_packet(m_AVCodecContext, m_Packet) == AVERROR_EOF) {
//解碼結束
result = -1;
goto __EXIT;
}
//一個 packet 包含多少 frame?
int frameCount = 0;
while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) {
//更新時間戳
UpdateTimeStamp();
//同步
AVSync();
//渲染
LOGCATE("DecoderBase::DecodeOnePacket 000 m_MediaType=%d", m_MediaType);
OnFrameAvailable(m_Frame);
LOGCATE("DecoderBase::DecodeOnePacket 0001 m_MediaType=%d", m_MediaType);
frameCount ++;
}
LOGCATE("BaseDecoder::DecodeOneFrame frameCount=%d", frameCount);
//判斷一個 packet 是否解碼完成
if(frameCount > 0) {
result = 0;
goto __EXIT;
}
}
av_packet_unref(m_Packet);
result = av_read_frame(m_AVFormatContext, m_Packet);
}
__EXIT:
av_packet_unref(m_Packet);
return result;
}
音視頻向系統時鍾同步
音視頻向系統時鍾同步,顧名思義,系統時鍾的更新是按照時間的增加而增加,獲取音視頻解碼幀時與系統時鍾進行對齊操作。
簡而言之就是,當前音頻或視頻播放時間戳大於系統時鍾時,解碼線程進行休眠,直到時間戳與系統時鍾對齊。
音視頻向系統時鍾同步。
void DecoderBase::AVSync() {
LOGCATE("DecoderBase::AVSync");
long curSysTime = GetSysCurrentTime();
//基於系統時鍾計算從開始播放流逝的時間
long elapsedTime = curSysTime - m_StartTimeStamp;
//向系統時鍾同步
if(m_CurTimeStamp > elapsedTime) {
//休眠時間
auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
av_usleep(sleepTime * 1000);
}
}
音視頻向系統時鍾同步可以最大限度減少丟幀跳幀現象,但是前提是系統時鍾不能受其他耗時任務影響。
音頻向視頻同步
音頻向視頻同步,就是音頻的時間戳向視頻的時間戳對齊。由於視頻有固定的刷新頻率,即 FPS ,我們根據 PFS 確定每幀的渲染時長,然后以此來確定視頻的時間戳。
當音頻時間戳大於視頻時間戳,或者超過一定的閾值,音頻播放器一般插入靜音幀、休眠或者放慢播放。反之,就需要跳幀、丟幀或者加快音頻播放。
void DecoderBase::AVSync() {
LOGCATE("DecoderBase::AVSync");
if(m_AVSyncCallback != nullptr) {
//音頻向視頻同步,傳進來的 m_AVSyncCallback 用於獲取視頻時間戳
long elapsedTime = m_AVSyncCallback(m_AVDecoderContext);
LOGCATE("DecoderBase::AVSync m_CurTimeStamp=%ld, elapsedTime=%ld", m_CurTimeStamp, elapsedTime);
if(m_CurTimeStamp > elapsedTime) {
//休眠時間
auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
av_usleep(sleepTime * 1000);
}
}
}
音頻向視頻同步時,解碼器設置。
//創建解碼器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);
//設置渲染器
m_VideoDecoder->SetVideoRender(OpenGLRender::GetInstance());
m_AudioRender = new OpenSLRender();
m_AudioDecoder->SetVideoRender(m_AudioRender);
//設置視頻時間戳回調
m_AudioDecoder->SetAVSyncCallback(m_VideoDecoder, VideoDecoder::GetVideoDecoderTimestampForAVSync);
音頻向視頻同步方式的優點是,視頻可以將每一幀播放出來,畫面流暢度最優。
但是由於人耳對聲音相對眼睛對圖像更為敏感,音頻在與視頻對齊時,插入靜音幀、丟幀或者變速播放操作,用戶可以輕易察覺,體驗較差。
視頻向音頻同步
視頻向音頻同步的方式比較常用,剛好利用了人耳朵對聲音變化比眼睛對圖像變化更為敏感的特點。
音頻按照固定的采樣率播放,為視頻提供對齊基准,當視頻時間戳大於音頻時間戳時,渲染器不進行渲染或者重復渲染上一幀,反之,進行跳幀渲染。
void DecoderBase::AVSync() {
LOGCATE("DecoderBase::AVSync");
if(m_AVSyncCallback != nullptr) {
//視頻向音頻同步,傳進來的 m_AVSyncCallback 用於獲取音頻時間戳
long elapsedTime = m_AVSyncCallback(m_AVDecoderContext);
LOGCATE("DecoderBase::AVSync m_CurTimeStamp=%ld, elapsedTime=%ld", m_CurTimeStamp, elapsedTime);
if(m_CurTimeStamp > elapsedTime) {
//休眠時間
auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
av_usleep(sleepTime * 1000);
}
}
}
音頻向視頻同步時,解碼器設置。
//創建解碼器
m_VideoDecoder = new VideoDecoder(url);
m_AudioDecoder = new AudioDecoder(url);
//設置渲染器
m_VideoDecoder->SetVideoRender(OpenGLRender::GetInstance());
m_AudioRender = new OpenSLRender();
m_AudioDecoder->SetVideoRender(m_AudioRender);
//設置音頻時間戳回調
m_VideoDecoder->SetAVSyncCallback(m_AudioDecoder, AudioDecoder::GetAudioDecoderTimestampForAVSync);
結語
播放器實現音視頻同步的這三種方式中,選擇哪一種方式合適要視具體的使用場景而定,比如你對畫面流暢度要求很高,可以選擇音頻向視頻同步;你要單獨實現視頻或音頻播放,直接向系統時鍾同步更為方便。
聯系與交流
技術交流獲取源碼可以添加我的微信:Byte-Flow