Qt/C++音視頻開發72-倍速推流/音視頻同步倍速推流/不改變幀率和採樣率/低倍速和高倍速

一、前言

最近多了個新需求,需要倍速推流,推流界的扛把子obs也有倍速推流功能,最高支持到兩倍速。這裏所說的倍速,當然只限定在文件,只有文件纔可能有倍速功能,因爲也只有文件才能倍速解碼播放。實時視頻流是不可能倍速的,因爲沒有時長,有時長的纔可以按照播放進度來。是否是文件也不能通過是不是本地文件等來判斷,以爲很多http/rtsp/m3u8等也可能是文件,具體最終的判斷依據應該是有沒有時長,能不能獲取到時長,能獲取到的就說明是文件。

倍速推流和倍速播放功能相通,在ffmpeg做音視頻解碼常識中,有個pts和dts就是用來控制顯示時間和解碼時間的,如果這兩個值除以2就說明時間少了一半,就是2倍速,乘以2就表示時間多了2倍,就是0.5倍速,基本上的運算公式就是 packet.pts = packet.pts/speed,其中這個speed速度參數是float類型。倍速播放的時候其實就是將收到的packet的pts/dts更改後,再送入解碼,而推流其實就是保存,保存到rtsp地址就是將數據推流到rtsp,所以將這個值經過同樣的運算髮出去,就形成了倍速推流。

公衆號:Qt實戰,各種開源作品、經驗整理、項目實戰技巧,專注Qt/C++軟件開發,視頻監控、物聯網、工業控制、嵌入式軟件、國產化系統應用軟件開發。

公衆號:Qt入門和進階,專門介紹Qt/C++相關知識點學習,幫助Qt開發者更好的深入學習Qt。多位Qt元嬰期大神,一步步帶你從入門到進階,走上財務自由之路。

二、效果圖

三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 體驗地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 文件名:bin_video_push。
  5. 視頻主頁:https://space.bilibili.com/687803542

四、功能特點

  1. 支持各種本地音視頻文件和網絡音視頻文件,格式包括mp3、aac、wav、wma、mp4、mkv、rmvb、wmv、mpg、flv、asf等。
  2. 支持各種網絡音視頻流,網絡攝像頭,協議包括rtsp、rtmp、http等。
  3. 支持本地攝像頭設備推流,可指定分辨率、幀率、格式等。
  4. 支持本地桌面採集推流,可指定屏幕索引、採集區域、起始座標、幀率等,也支持指定窗口標題進行採集。
  5. 可實時切換預覽視頻文件,可切換音視頻文件播放進度,切換到哪裏就推流到哪裏。預覽過程中可以切換靜音狀態和暫停推流。
  6. 可指定重新編碼推流,任意源頭格式可選強轉264或265格式。
  7. 可轉換分辨率推流,設置等比例縮放或者指定分辨率進行轉換。
  8. 推流的清晰度、質量、碼率都可調,可以節約網絡帶寬和拉流端的壓力。
  9. 音視頻文件自動循環不間斷推流。
  10. 音視頻流有自動掉線重連機制,重連成功自動繼續推流。
  11. 支持各種流媒體服務程序,包括但不限於mediamtx、ZLMediaKit、srs、LiveQing、nginx-rtmp、EasyDarwin、ABLMediaServer。
  12. 通過配置文件自動加載對應流媒體程序的協議和端口,自動生成推流地址和各種協議的拉流地址。可以通過配置文件自己增加流媒體程序。
  13. 可選rtmp、rtmp格式推流,推流成功後,支持多種格式拉流,包括但不限於rtsp、rtmp、hls、flv、ws-flv、webrtc等。
  14. 在軟件上推流成功後,可以直接單擊網頁預覽,實時預覽推流後拉流的畫面,多畫面網頁展示。
  15. 軟件界面上可單擊對應按鈕,動態添加文件和目錄,可手動輸入地址。
  16. 推拉流實時性極高,延遲極低,延遲時間大概在100ms左右。
  17. 極低CPU資源佔用,4路主碼流推流只需要佔用0.2%CPU。理論上常規普通PC機器推100路毫無壓力,主要性能瓶頸在網絡。
  18. 可以推流到外網服務器,然後通過手機、電腦、平板等設備播放對應的視頻流。
  19. 每路推流都可以手動指定唯一標識符(方便拉流/用戶無需記憶複雜的地址),沒有指定則按照策略隨機生成hash值。也支持自動按照指定標識後面加數字的方式遞增命名。比如設置標識爲字母v,策略爲標識遞增,則每添加一個對應的推流碼命名依次是v1、v2、v3等。
  20. 根據推流協議自動轉碼格式,默認策略按照選擇的推流協議,比如rtsp支持265而rtmp不支持,如果是265的文件而選擇rtmp推流,則自動轉碼成264格式再推流。
  21. 音視頻同步推流,在拉流和採集的時候就會自動處理好同步,同步後的數據再推流。
  22. 表格中實時顯示每一路推流的分辨率和音視頻數據狀態,灰色表示沒有輸入流,黑色表示沒有輸出流,綠色表示原數據推流,紅色表示轉碼後的數據推流。
  23. 自動重連視頻源,自動重連流媒體服務器,保證啓動後,推流地址和打開地址都實時重連,只要恢復後立即連上繼續採集和推流。
  24. 根據不同的流媒體服務器類型,自動生成對應的rtsp、rtmp、hls、flv、ws-flv、webrtc拉流地址,用戶可以直接複製該地址到播放器或者網頁中預覽查看。
  25. 添加的推流地址等信息自動存儲到文件,可以手動打開進行修改,默認啓動後自動加載歷史記錄。
  26. 可以指定生成的網頁文件保存位置,方便作爲網站網頁發佈,可以直接在瀏覽器中輸入網址進行訪問,發佈後可以直接在局域網其他設備比如手機或者電腦打開對應網址訪問。
  27. 可選是否開機啓動、後臺運行等。網絡推流添加的rtsp地址可勾選是否隱藏地址中的用戶信息。
  28. 自帶設備推流模塊,自動識別本地設備,包括本地的攝像頭和桌面,可以手動選擇不同的是視頻和音頻採集設備進行推流。
  29. 自帶文件點播模塊,添加文件後用戶可以拉取地址點播,用戶端可以任意切換播放進度。支持各種瀏覽器(谷歌chromium、微軟edge、火狐firefox等)、各種播放器(vlc、mpv、ffplay、potplayer、mpchc等)打開請求。
  30. 文件點播模塊實時統計顯示每個文件對應的訪問數量、總訪問數量、不同IP地址訪問數量。
  31. 文件點播模塊採用純QTcpSocket通信,不依賴流媒體服務程序,核心源碼不到500行,註釋詳細,功能完整。
  32. 支持任意Qt版本(Qt4、Qt5、Qt6),支持任意系統(windows、linux、macos、android、嵌入式linux等)。

五、相關代碼

#include "frmspeedpush.h"
#include "ui_frmspeedpush.h"
#include "qthelper.h"
#include "videoutil.h"

frmSpeedPush::frmSpeedPush(QWidget *parent) : QWidget(parent), ui(new Ui::frmSpeedPush)
{
    ui->setupUi(this);
    this->initForm();
    this->initConfig();
}

frmSpeedPush::~frmSpeedPush()
{
    AppConfig::SpeedPushStart = (ui->btnStart->text() == "停止推流");
    AppConfig::writeConfig();
    delete ui;
}

void frmSpeedPush::initForm()
{
    VideoPara videoPara = ui->videoWidget->getVideoPara();
    videoPara.videoCore = VideoCore_FFmpeg;
    ui->videoWidget->setVideoPara(videoPara);

    VideoPara para = ui->videoWidget->getVideoPara();
    para.playRepeat = true;
    ui->videoWidget->setVideoPara(para);

    connect(ui->videoWidget, SIGNAL(sig_receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
    connect(ui->videoWidget, SIGNAL(sig_receivePlayFinsh()), this, SLOT(receivePlayFinsh()));
}

void frmSpeedPush::initConfig()
{
    VideoUtil::loadMediaUrl(ui->cboxMediaUrl, AppConfig::SpeedMediaUrl, 0x40);
    connect(ui->cboxMediaUrl->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    ui->txtPushUrl->setText(AppConfig::SpeedPushUrl);
    connect(ui->txtPushUrl, SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    VideoUtil::loadSpeed(ui->cboxSpeed);
    ui->cboxSpeed->setCurrentIndex(ui->cboxSpeed->findData(AppConfig::SpeedPushValue));
    connect(ui->cboxSpeed, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));

    ui->ckMuted->setChecked(AppConfig::SpeedPushMuted);
    connect(ui->ckMuted, SIGNAL(stateChanged(int)), this, SLOT(saveConfig()));

    if (AppConfig::SpeedPushStart) {
        on_btnStart_clicked();
    }
}

void frmSpeedPush::saveConfig()
{
    AppConfig::SpeedMediaUrl = ui->cboxMediaUrl->currentText().trimmed();
    AppConfig::SpeedPushUrl = ui->txtPushUrl->text().trimmed();
    AppConfig::SpeedPushValue = ui->cboxSpeed->itemData(ui->cboxSpeed->currentIndex()).toFloat();
    AppConfig::SpeedPushMuted = ui->ckMuted->isChecked();
    AppConfig::writeConfig();
}

void frmSpeedPush::receivePlayStart(int time)
{
    VideoThread *thread = ui->videoWidget->getVideoThread();
    thread->setMuted(AppConfig::SpeedPushMuted);
    thread->setSpeed(AppConfig::SpeedPushValue);
    thread->setEncodeSpeed(AppConfig::SpeedPushValue);
    thread->recordStart(AppConfig::SpeedPushUrl);
    connect(thread, SIGNAL(receivePosition(qint64)), this, SLOT(receivePosition(qint64)));

    ui->sliderPosition->setRange(0, thread->getDuration());
    ui->btnStart->setText("停止推流");
    ui->cboxSpeed->setEnabled(false);
}

void frmSpeedPush::receivePlayFinsh()
{
    ui->sliderPosition->setRange(0, 0);
    ui->btnStart->setText("啓動推流");
    ui->cboxSpeed->setEnabled(true);
}

void frmSpeedPush::receivePosition(qint64 position)
{
    ui->sliderPosition->setValue(position);
}

void frmSpeedPush::on_btnStart_clicked()
{
    if (ui->btnStart->text() == "啓動推流") {
        ui->videoWidget->open(AppConfig::SpeedMediaUrl);
    } else {
        ui->videoWidget->stop();
    }
}

void frmSpeedPush::on_sliderPosition_clicked()
{
    int value = ui->sliderPosition->value();
    on_sliderPosition_sliderMoved(value);
}

void frmSpeedPush::on_sliderPosition_sliderMoved(int value)
{
    ui->videoWidget->setPosition(value);
}

void frmSpeedPush::on_ckMuted_stateChanged(int arg1)
{
    ui->videoWidget->setMuted(ui->ckMuted->isChecked());
}

void FFmpegSave::writePacket2(AVPacket *packet)
{
    //非音視頻流不用處理
    int index = packet->stream_index;
    if (index != videoIndexOut && index != audioIndexOut) {
        return;
    }

    //轉發數據包(可以設置僅僅轉發數據包不用繼續)
    if (sendPacket) {
        emit receivePacket(FFmpegHelper::creatPacket(packet));
        if (onlySendPacket) {
            return;
        }
    }

    //封裝格式 https://blog.csdn.net/weixin_44520287/article/details/113435440 https://xilixili.net/2018/08/20/ffmpeg-got-raw-h264/
    //測試發現部分文件如果是非編碼保存也寫入了/可能部分播放器不支持保存後的文件播放/比如安卓上
    if (index == videoIndexOut) {
        FFmpegSaveHelper::writeBsf(packet, videoStreamIn, bsfCtx);
    }

    if (saveVideoType == SaveVideoType_Stream) {
        //只需要寫入視頻數據
        if (index == videoIndexOut) {
            file.write((char *)packet->data, packet->size);
        }
    } else if (saveVideoType == SaveVideoType_Mp4) {
        //取出輸入輸出流的時間基
        AVStream *streamIn = (index == videoIndexOut ? videoStreamIn : audioStreamIn);
        AVStream *streamOut = formatCtx->streams[index];
        AVRational timeBaseIn = streamIn->time_base;
        AVRational timeBaseOut = streamOut->time_base;

        //轉換時間基準
        if (index == videoIndexOut) {
            FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, videoCount, frameRate);
            FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut);
        } else if (index == audioIndexOut) {
            if (audioEncode) {
                FFmpegSaveHelper::rescalePacket(packet, audioDuration);
            } else {
                FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut, audioDuration);
            }
        }

        //打印對應的信息方便查看/videoIndexOut/audioIndexOut
        if (index == -1) {
            qDebug() << TIMEMS << flag << index << packet->pts << packet->dts << packet->duration;
        }

        //倍速調整時間戳
        if (encodeSpeed != 1) {
            packet->pts = packet->pts / encodeSpeed;
            packet->dts = packet->dts / encodeSpeed;
        }

        //寫入一幀數據/如果用 av_interleaved_write_frame 默認會緩存/可能導致音頻越來越慢
        int result = av_write_frame(formatCtx, packet);
        if (result < 0) {
            errorCount++;
            debug(result, QString("寫%1包").arg(index == audioIndexOut ? "音頻" : "視頻"), "");
        } else {
            errorCount = 0;
        }

        //推流超過錯誤次數需要重連
        if (errorCount >= 5 && saveMode != SaveMode_File) {
            isOk = false;
            errorCount = 0;
            emit receiveSaveError(VideoError_Save);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章