天天看點

FFmpeg 開發(06):FFmpeg 播放器實作音視訊同步的三種方式音視訊解碼器結構音視訊向系統時鐘同步音頻向視訊同步視訊向音頻同步結語聯系與交流

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/108308154 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 種,即:音視訊向系統時鐘同步、音頻向視訊同步及視訊向音頻同步。

音視訊解碼器結構

在實作音視訊同步之前,我們先簡單說下本文播放器的大緻結構,友善後面實作不同的音視訊同步方式。

FFmpeg 開發(06):FFmpeg 播放器實作音視訊同步的三種方式音視訊解碼器結構音視訊向系統時鐘同步音頻向視訊同步視訊向音頻同步結語聯系與交流

如上圖所示,音頻解碼和視訊解碼分别占用一個獨立線程,線程裡有一個解碼循環,解碼循環裡不斷對音視訊編碼資料進行解碼,音視訊解碼幀不設定緩存 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

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
FFmpeg 開發(06):FFmpeg 播放器實作音視訊同步的三種方式音視訊解碼器結構音視訊向系統時鐘同步音頻向視訊同步視訊向音頻同步結語聯系與交流

繼續閱讀