(五)Android通過ffmpeg,實現(xiàn)音視頻同步

前面已經介紹過視頻的解碼與顯示,和音頻的解碼與播放了。但這里會有一個問題,那就是視頻和音頻的同步。

不同步有什么后果?

后果就是要么視頻播放太快了,音頻沒有跟上;或者音頻播放太快了,視頻沒有跟上;嚴重影響整體的觀看體驗。
就好比小姐姐當面問你聯(lián)系方式,小姐姐你看到了,但人家說的啥你還沒聽到,之后人家都走了,你才聽到原來是問電話號碼的,多慘。

那怎么解決呢?

方法一:以音頻的解碼流為主參照,視頻流的解碼向音頻的解碼時間靠攏。
方法二:以視頻的解碼流為主參照,音頻流的解碼視頻的解碼時間靠攏。
方法三:以手機系統(tǒng)時間為主參照,視頻流和音頻流的解碼都向系統(tǒng)時間靠攏。

哪一種方法最好?或者說,每種方法的使用場景是什么?

我的理解是:以誰為主參照,就是看重誰。
如果聲音斷開一下下,我們的耳朵是很容易感覺出來的,相反如果聲音連續(xù),視頻幀偶爾卡一下下,一般都影響不大。當需求是極度要求聲音的連續(xù)性的,那就方法一。
相反,當需求是極度要求視頻的連續(xù)性的,那就方法二。
至于方法三,就是折中的方法,感覺啥時候都可以用,我自己也大多數(shù)用方法三。

如何實現(xiàn)?

在這之前,先介紹幾個屬性。
I幀:關鍵幀,幀內編碼幀 又稱intra picture,I 幀通常是每個 GOP(MPEG 所使用的一種視頻壓縮技術)的第一個幀,經過適度地壓縮,做為隨機訪問的參考點,可以當成圖象。I幀可以看成是一個圖像經過壓縮后的產物??瑟毩⒔獯a。
B幀:雙向預測內插編碼幀 又稱bi-directional interpolated prediction frame,可以大大提高壓縮倍數(shù)。(與I幀相似度95%以上)
P幀:前向預測編碼幀 又稱predictive-frame,P 幀圖像只采用前向時間預測,可以提高壓縮效率和圖像質量。(與I幀相似度70%以上)
DTS:幀數(shù)據(jù)的編碼時間戳,這個時間戳的意義在于告訴播放器該在什么時候解碼這一幀的數(shù)據(jù)。
PTS:幀數(shù)據(jù)的顯示時間戳,這個時間戳用來告訴播放器該在什么時候顯示這一幀的數(shù)據(jù)。
怎么理解這些東西?直接復制網絡上的一張圖比較直觀和方便。

20220314143821.png

上圖就是一個GOP內的幀數(shù)據(jù),通過PTS和DTS,播放器可以知道在某一個時間,解碼哪一幀,顯示哪一幀。
同樣的,當有2個流(視頻流和音頻流),我們需要他們的進度保持相對的一致,那么只要保證他們的PTS或者DTS都相對一致,就可以了。

具體做法

/**
 * 解碼一幀數(shù)據(jù)
 * @return 0 if OK, < 0 on error or end of file
 */
int BaseDecoder::DecodeOnePacket() {
    if (m_SeekPosition > 0) {//拖動進度條

    }
    //讀取一幀數(shù)據(jù)到 m_Packet 中
    int result = av_read_frame(m_AVFormatContext, m_Packet);
    while (result == 0) {
        //匹配幀的index
        if (m_Packet->stream_index == m_StreamIndex) {

            if (avcodec_send_packet(m_AVCodecContext, m_Packet) == AVERROR_EOF) {
                //解碼結束
                result = -1;
                goto __EXIT;
            }

            int frameCount = 0;
            while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) {
                //更新時間戳
                UpdateTimeStamp();
                //同步
                AVSync();
                //渲染視頻
                OnFrameAvailable(m_Frame);
                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;
}

這段代碼的主要功能,就是解碼一幀數(shù)據(jù)(視頻幀、音頻幀都可以),然后交給對應的模塊去顯示和播放聲音。其中的UpdateTimeStamp()和AVSync()就是同步的主要方法了。


void BaseDecoder::UpdateTimeStamp() {
    LOGE("DecoderBase::UpdateTimeStamp");
    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);

    if(m_SeekPosition > 0 && m_SeekSuccess)
    {
        m_StartTimeStamp = GetSysCurrentTime() - m_CurTimeStamp;
        m_SeekPosition = 0;
        m_SeekSuccess = false;
    }
}

long BaseDecoder::AVSync() {
    LOGD("BaseDecoder::AVSync");
    long curSysTime = GetSysCurrentTime();
    //基于系統(tǒng)時鐘計算從開始播放流逝的時間
    long elapsedTime = curSysTime - m_StartTimeStamp;
    long delay = 0;
    //向系統(tǒng)時鐘同步
    if(m_CurTimeStamp > elapsedTime) {
        //休眠時間
        auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
        //限制休眠時間不能過長
        sleepTime = sleepTime > DELAY_THRESHOLD ? DELAY_THRESHOLD :  sleepTime;
        av_usleep(sleepTime * 1000);
    }
    delay = elapsedTime - m_CurTimeStamp;
    return delay;
}

每次解碼都更新一下時間,然后跟系統(tǒng)時間做對比,根據(jù)時間差進行判斷,要么睡眠等待,要么繼續(xù)執(zhí)行解碼。

還有需要注意的不?

首先,睡眠等待的時間,需要一個閾值,不然很影響體驗。
然后,如果輸入流是網絡,輸入數(shù)據(jù)是不穩(wěn)定的,有可能會丟幀之類的情況出現(xiàn),這些情況需要根據(jù)具體需求去建立方案解決,這里就不寫了。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容