Qt音視訊開發24-ffmpeg音視訊同步

2020-09-25 12:00:12

一、前言

用ffmpeg來做音視訊同步,個人認為這個是ffmpeg基礎處理中最難的一個,無數人就卡在這裡,怎麼也不準,本人也是嘗試過網上各種demo,基本上都是渣渣,要麼僅僅支援極其少量的視訊檔比如收到的封包是一幀視訊一幀音訊的,要麼根本沒法同步歪七八糟的,要麼進度跳過去直接蹦蹦蹦崩潰的,其實最完美的音視訊同步處理demo就是ffplay,我親測過幾十種各種各樣的音視訊本地檔案,數十種視訊流檔案,都是非常完美,當然啦這是親生的啦,不完美還玩個屁。

如果僅僅是播放視訊流(不帶音訊流),可能不需要音視訊同步,所以最開始只做rtsp視訊流播放的時候根本沒有考慮同步的問題,因為沒遇到也不需要,等到後期發現各種rtmp、http、m3u8這種視訊流的時候,問題大了去了,他是hls格式的視訊流檔案一次性過來的,一個個小視訊檔過來的,如果沒有同步的話,意味著突然之間刷刷刷的圖片過去很多,下一次來的又是刷刷的,這就需要自己計算同步了,上次接收到的封包放入佇列,到了需要顯示的時候就顯示。

常用的音視訊同步方法:

  1. 通過fps來控制,fps表示一秒鐘播放多少幀,比如25幀,可以自行計算一幀解碼用掉的時間,一幀佔用(1000/25=40毫秒),通過延時來處理,這其實是最渣渣的辦法。
  2. 記住開始解碼的時間startTime,通過av_rescale_q計算pts時間,兩者的差值就是需要延時的時間,呼叫av_usleep來延時,這種只有部分檔案正常,很多時候不正常。
  3. 音訊同步到視訊,視訊時鐘作為主時鐘,沒試過,網上很多人說這個辦法不好。
  4. 視訊同步到音訊,音訊時鐘作為主時鐘,沒試過,據說大部分人採用的此辦法。
  5. 音視訊同步到外部時鐘,外部時鐘作為主時鐘,最終採用的辦法,容易理解互不干擾,各自按照外部時鐘去同步自己。
  6. ffplay自身內建了三種同步策略,可以通過引數來控制採用何種策略,預設是視訊同步到音訊。

二、功能特點

  1. 多執行緒實時播放視訊流+本地視訊+USB攝像頭等。
  2. 支援windows+linux+mac,支援ffmpeg3和ffmpeg4,支援32位元和64位元。
  3. 多執行緒顯示影象,不卡主介面。
  4. 自動重連網路攝像頭。
  5. 可設定邊框大小即偏移量和邊框顏色。
  6. 可設定是否繪製OSD標籤即標籤文字或圖片和標籤位置。
  7. 可設定兩種OSD位置和風格。
  8. 可設定是否儲存到檔案以及檔名。
  9. 可直接拖曳檔案到ffmpegwidget控制元件播放。
  10. 支援h265視訊流+rtmp等常見視訊流。
  11. 可暫停播放和繼續播放。
  12. 支援儲存單個視訊檔和定時儲存視訊檔。
  13. 自定義頂部懸浮條,傳送單擊訊號通知,可設定是否啟用。
  14. 可設定畫面拉伸填充或者等比例填充。
  15. 可設定解碼是速度優先、品質優先、均衡處理。
  16. 可對視訊進行截圖(原始圖片)和截圖。
  17. 錄影檔案儲存支援裸流和MP4檔案。
  18. 音視訊完美同步,採用外部時鐘同步策略。
  19. 支援seek定位播放位置。
  20. 支援qsv、dxva2、d3d11va等硬解碼。
  21. 支援opengl繪製視訊資料,極低CPU佔用。
  22. 支援安卓和嵌入式linux,交叉編譯即可。

三、效果圖

在這裡插入圖片描述

四、相關站點

  1. 國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
  2. 國際站點:https://github.com/feiyangqingyun/QWidgetDemo
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun/
  5. 體驗地址:https://blog.csdn.net/feiyangqingyun/article/details/97565652

五、核心程式碼

void FFmpegSync::run()
{
    reset();
    while (!stopped) {
        //暫停狀態或者佇列中沒有幀則不處理
        if (!thread->isPause && packets.count() > 0) {
            mutex.lock();
            AVPacket *packet = packets.first();
            mutex.unlock();

            //h264的裸流檔案同步有問題,獲取不到pts和dts,暫時用最蠢的辦法延時解決
            if (thread->formatName == "h264") {
                int sleepTime = (1000 / thread->videoFps) - 5;
                msleep(sleepTime);
            }

            //計算當前幀顯示時間 外部時鐘同步
            ptsTime = getPtsTime(thread->formatCtx, packet);
            if (!this->checkPtsTime()) {
                msleep(1);
                continue;
            }

            //顯示當前的播放進度
            checkShowTime();

            //0-表示音訊 1-表示視訊
            if (type == 0) {
                thread->decodeAudio(packet);
            } else if (type == 1) {
                thread->decodeVideo(packet);
            }

            //釋放資源並移除
            thread->free(packet);
            mutex.lock();
            packets.removeFirst();
            mutex.unlock();
        }

        msleep(1);
    }

    clear();
    stopped = false;
}

bool FFmpegSync::checkPtsTime()
{
    bool ok = false;
    if (ptsTime > 0) {
        if (ptsTime > offsetTime + 100000) {
            bufferTime = ptsTime - offsetTime + 100000;
        }

        int offset = (type == 0 ? 1000 : 5000);
        offsetTime = av_gettime() - startTime + bufferTime;
        if ((offsetTime <= ptsTime && ptsTime - offsetTime <= offset) || (offsetTime > ptsTime)) {
            ok = true;
        }
    } else {
        ok = true;
    }

    return ok;
}