首頁 > 軟體

Qt音視訊開發之視訊檔儲存功能的實現

2022-12-07 14:01:31

一、前言

和音訊儲存類似,視訊的儲存也對應三種格式,視訊最原始的資料是yuv(音訊對應pcm),視訊壓縮後的資料是h264(音訊對應aac),由於很多播放器或者早期的播放器不支援直接播放h264檔案,所以需要用編碼器編碼成mp4格式,這塊就需要用到ffmpeg裡面一整套的編碼流程,對yuv資料進行編碼成MP4格式儲存。

在經過對各種視訊檔或者視訊流儲存的過程中,發現rtsp這類的視訊流可以直接編碼打包儲存,不需要經過 avcodec_send_frame avcodec_receive_packet 這兩個步驟對每個包編碼,這樣可以極大的降低CPU佔用,猜測可能是rtsp視訊流收到的封包packet就已經是標準的h264裸流帶了各種pps啥的。所以視訊的監控領域如果要同時儲存16路32路視訊,採用這個策略是最穩妥的,相當於一直寫檔案。很多人會覺得編碼流程繁瑣,其實只要靜下心來,挨個測試,把流程搞懂,基本上都是水到渠成的事情。包括之前遇到的儲存的檔案滑鼠右鍵屬性中看不到解析度等引數資訊,原來是呼叫寫入檔案頭 avformat_write_header 寫入的時機不對,一定要在開啟開啟視訊編碼器 avcodec_open2 以及開啟輸出檔案 avio_open 以後再寫入。

編碼儲存的大致流程:

  • 查詢編碼器 avcodec_find_encoder
  • 建立編碼器 avcodec_alloc_context3
  • 設定編碼器 pix_fmt/time_base/framerate/width/height
  • 開啟編碼器 avcodec_open2
  • 建立上下文 avformat_alloc_output_context2
  • 建立輸出流 avformat_new_stream
  • 設定流引數 avcodec_parameters_from_context
  • 寫入開始符 avformat_write_header
  • 傳送資料框 avcodec_send_frame
  • 打包資料框 avcodec_receive_packet
  • 寫入資料框 av_interleaved_write_frame
  • 寫入結尾符 av_write_trailer
  • 釋放各資源 avcodec_free_context/avio_close/avformat_free_context

二、效果圖

三、體驗地址

國內站點:https://gitee.com/feiyangqingyun

國際站點:https://github.com/feiyangqingyun

體驗地址:https://pan.baidu.com/s/1bNGaPFZc7UovQI57UIQSbw 提取碼:xk9a 檔名:bin_video_demo/bin_linux_video。

四、相關程式碼

bool FFmpegSave::initVideoH264()
{
    //查詢視訊編碼器(如果源頭是H265則採用HEVC作為編碼器)
    AVCodecID codecID = FFmpegHelper::getCodecID(videoStreamIn);
    if (codecID == AV_CODEC_ID_HEVC) {
        videoCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
    } else {
        videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    }

    if (!videoCodec) {
        debug("編碼失敗", QString("錯誤: 查詢視訊編碼器失敗"));
        return false;
    }

    //建立視訊編碼器上下文
    videoCodecCtx = avcodec_alloc_context3(videoCodec);
    if (!videoCodecCtx) {
        debug("編碼失敗", QString("錯誤: 建立視訊編碼器上下文失敗"));
        return false;
    }

    //為了相容低版本的編譯器推薦選擇第一種方式
#if 1
    //放大係數是為了小數位能夠正確放大到整型
    int ratio = 10000;
    videoCodecCtx->time_base.num = 1 * ratio;
    videoCodecCtx->time_base.den = frameRate * ratio;
    videoCodecCtx->framerate.num = frameRate * ratio;
    videoCodecCtx->framerate.den = 1 * ratio;
#elif 0
    videoCodecCtx->time_base = {1, frameRate};
    videoCodecCtx->framerate = {frameRate, 1};
#else
    videoCodecCtx->time_base = videoStreamIn->codec->time_base;
    videoCodecCtx->framerate = videoStreamIn->codec->framerate;
#endif

#if 0
    videoCodecCtx->qmin = 10;
    videoCodecCtx->qmax = 51;
    videoCodecCtx->me_range = 16;
    videoCodecCtx->max_qdiff = 4;
    videoCodecCtx->qcompress = 0.6;
#endif

    //初始化視訊編碼器引數(如果要檔案體積小一些畫質差一些可以調整位元速率)
    //引數說明 https://blog.csdn.net/qq_40179458/article/details/110449653
    videoCodecCtx->bit_rate = FFmpegHelper::getBitRate(videoWidth, videoHeight);
    videoCodecCtx->width = videoWidth;
    videoCodecCtx->height = videoHeight;
    videoCodecCtx->gop_size = 25;
    videoCodecCtx->max_b_frames = 3;
    videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    videoCodecCtx->level = 50;
    videoCodecCtx->profile = FF_PROFILE_H264_MAIN;

    //加上下面這個才能在檔案屬性中看到解析度等資訊 https://www.cnblogs.com/lidabo/p/15754031.html
    if (saveVideoType == SaveVideoType_Mp4) {
        videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    //載入預設 https://blog.csdn.net/JineD/article/details/125304570
    if (videoCodecCtx->codec_id == AV_CODEC_ID_H264) {
        av_opt_set(videoCodecCtx->priv_data, "preset", "slow", 0);
        //設定零延遲(本地攝像頭視訊流儲存如果不設定則播放的時候會越來越模糊)
        av_opt_set(videoCodecCtx->priv_data, "tune", "zerolatency", 0);
    } else if (videoCodecCtx->codec_id == AV_CODEC_ID_HEVC) {
        av_opt_set(videoCodecCtx->priv_data, "x265-params", "qp=20", 0);
        av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0);
        av_opt_set(videoCodecCtx->priv_data, "tune", "zero-latency", 0);
    }

    //開啟視訊編碼器
    int result = avcodec_open2(videoCodecCtx, videoCodec, NULL);
    if (result < 0) {
        debug("開啟編碼", QString("錯誤: 開啟視訊編碼器失敗 %1").arg(FFmpegHelper::getError(result)));
        return false;
    }

    videoPacket = FFmpegHelper::creatPacket(NULL);
    return true;
}

bool FFmpegSave::initVideoMp4()
{
    //必須先設定過輸入視訊流
    if (!videoStreamIn || fileName.isEmpty()) {
        return false;
    }

    //有部分視訊引數不正確儲存不了 http://tv.netxt.cc:1998/live/y.flv
    if (videoStreamIn->time_base.num == 0) {
        return false;
    }

    QByteArray fileData = fileName.toUtf8();
    const char *filename = fileData.data();

    //開闢一個格式上下文用來處理視訊流輸出
    int result = avformat_alloc_output_context2(&formatCtx, NULL, "mp4", filename);
    if (result < 0) {
        debug("建立格式", QString("錯誤: %1").arg(FFmpegHelper::getError(result)));
        return false;
    }

    //建立視訊流用來輸出視訊資料到檔案
    videoStreamOut = avformat_new_stream(formatCtx, NULL);
    result = FFmpegHelper::copyContext(videoCodecCtx, videoStreamOut, true);
    if (result < 0) {
        debug("建立視訊", QString("錯誤: %1").arg(FFmpegHelper::getError(result)));
        goto end;
    }

    //開啟輸出檔案
    result = avio_open(&formatCtx->pb, filename, AVIO_FLAG_WRITE);
    if (result < 0) {
        debug("開啟輸出", QString("錯誤: %1").arg(FFmpegHelper::getError(result)));
        goto end;
    }

    //寫入檔案開始符
    result = avformat_write_header(formatCtx, NULL);
    if (result < 0) {
        debug("寫入失敗", QString("錯誤: %1").arg(FFmpegHelper::getError(result)));
        goto end;
    }

    return true;

end:
    //關閉釋放並清理檔案
    this->close();
    this->deleteFile(fileName);
    return false;
}

void FFmpegSave::writePacket(AVPacket *packet)
{
    packetCount++;
    if (saveVideoType == SaveVideoType_H264) {
        file.write((char *)packet->data, packet->size);
    } else if (saveVideoType == SaveVideoType_Mp4) {
        AVRational timeBaseIn = videoStreamIn->time_base;
        AVRational timeBaseOut = videoStreamOut->time_base;

        //沒有下面這段判斷在遇到不連續的幀的時候就會錯位(相當於每次重新計算時間基準保證時間正確)
        //不連續幀的情況有暫停錄製以及切換播放進度導致中間有些幀不需要錄製
        double fps = frameRate;//av_q2d(videoStreamIn->r_frame_rate);
        double duration = AV_TIME_BASE / fps;
        packet->pts = (packetCount * duration) / (av_q2d(timeBaseIn) * AV_TIME_BASE);
        packet->dts = packet->pts;
        packet->duration = duration / (av_q2d(timeBaseIn) * AV_TIME_BASE);

        //重新調整時間基準
        packet->pts = av_rescale_q_rnd(packet->pts, timeBaseIn, timeBaseOut, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet->dts = packet->pts;
        //packet->dts = av_rescale_q_rnd(packet->dts, timeBaseIn, timeBaseOut, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet->duration = av_rescale_q(packet->duration, timeBaseIn, timeBaseOut);
        packet->pos = -1;

        //寫入一幀資料
        int result = av_interleaved_write_frame(formatCtx, packet);
        if (result < 0) {
            debug("寫入失敗", QString("錯誤: %1").arg(FFmpegHelper::getError(result)));
        }
    }

    av_packet_unref(packet);
}

五、功能特點

5.1 基礎功能

  • 支援各種音訊視訊檔格式,比如mp3、wav、mp4、asf、rm、rmvb、mkv等。
  • 支援本地攝像頭裝置,可指定解析度、影格率。
  • 支援各種視訊流格式,比如rtp、rtsp、rtmp、http等。
  • 本地音視訊檔和網路音視訊檔,自動識別檔案長度、播放進度、音量大小、靜音狀態等。
  • 檔案可以指定播放位置、調節音量大小、設定靜音狀態等。
  • 支援倍速播放檔案,可選0.5倍、1.0倍、2.5倍、5.0倍等速度,相當於慢放和快放。
  • 支援開始播放、停止播放、暫停播放、繼續播放。
  • 支援抓拍截圖,可指定檔案路徑,可選抓拍完成是否自動顯示預覽。
  • 支援錄影儲存,手動開始錄影、停止錄影,部分核心支援暫停錄影後繼續錄影,跳過不需要錄影的部分。
  • 支援無感知切換回圈播放、自動重連等機制。
  • 提供播放成功、播放完成、收到解碼圖片、收到抓拍圖片、視訊尺寸變化、錄影狀態變化等訊號。
  • 多執行緒處理,一個解碼一個執行緒,不卡主介面。

5.2 特色功能

  • 同時支援多種解碼核心,包括qmedia核心(Qt4/Qt5/Qt6)、ffmpeg核心(ffmpeg2/ffmpeg3/ffmpeg4/ffmpeg5)、vlc核心(vlc2/vlc3)、mpv核心(mpv1/mp2)、海康sdk、easyplayer核心等。
  • 非常完善的多重基礎類別設計,新增一種解碼核心只需要實現極少的程式碼量,就可以應用整套機制。
  • 同時支援多種畫面顯示策略,自動調整(原始解析度小於顯示控制元件尺寸則按照原始解析度大小顯示,否則等比例縮放)、等比例縮放(永遠等比例縮放)、拉伸填充(永遠拉伸填充)。所有核心和所有視訊顯示模式下都支援三種畫面顯示策略。
  • 同時支援多種視訊顯示模式,控制程式碼模式(傳入控制元件控制程式碼交給對方繪製控制)、繪製模式(回撥拿到資料後轉成QImage用QPainter繪製)、GPU模式(回撥拿到資料後轉成yuv用QOpenglWidget繪製)。
  • 支援多種硬體加速型別,ffmpeg可選dxva2、d3d11va等,mpv可選auto、dxva2、d3d11va,vlc可選any、dxva2、d3d11va。不同的系統環境有不同的型別選擇,比如linux系統有vaapi、vdpau,macos系統有videotoolbox。
  • 解碼執行緒和顯示錶單分離,可指定任意解碼核心掛載到任意顯示錶單,動態切換。
  • 支援共用解碼執行緒,預設開啟並且自動處理,當識別到相同的視訊地址,共用一個解碼執行緒,在網路視訊環境中可以大大節約網路流量以及對方裝置的推流壓力。國內頂尖視訊廠商均採用此策略。這樣只要拉一路視訊流就可以共用到幾十個幾百個通道展示。
  • 自動識別視訊旋轉角度並繪製,比如手機上拍攝的視訊一般是旋轉了90度的,播放的時候要自動旋轉處理,不然預設是倒著的。
  • 自動識別視訊流播放過程中解析度的變化,在視訊控制元件上自動調整尺寸。比如攝像機可以在使用過程中動態設定解析度,當解析度改動後對應視訊控制元件也要做出同步反應。
  • 音視訊檔無感知自動切換回圈播放,不會出現切換期間黑畫面等肉眼可見的切換痕跡。
  • 視訊控制元件同時支援任意解碼核心、任意畫面顯示策略、任意視訊顯示模式。
  • 視訊控制元件懸浮條同時支援控制程式碼、繪製、GPU三種模式,非絕對座標移來移去。
  • 本地攝像頭裝置支援指定裝置名稱、解析度、影格率進行播放。
  • 錄影檔案同時支援開啟的視訊檔、本地攝像頭、網路視訊流等。
  • 瞬間響應開啟和關閉,無論是開啟不存在的視訊或者網路流,探測裝置是否存在,讀取中的超時等待,收到關閉指令立即中斷之前的操作並響應。
  • 支援開啟各種圖片檔案,支援本地音視訊檔拖曳播放。
  • 視訊控制元件懸浮條自帶開始和停止錄影切換、聲音靜音切換、抓拍截圖、關閉視訊等功能。
  • 音訊元件支援聲音波形值資料解析,可以根據該值繪製波形曲線和柱狀聲音條,預設提供了聲音振幅訊號。
  • 各元件中極其詳細的列印資訊提示,尤其是報錯資訊提示,封裝的統一列印格式。針對現場複雜的裝置環境測試極其方便有用,相當於精確定位到具體哪個通道哪個步驟出錯。
  • 程式碼框架和結構優化到最優,效能強悍,持續迭代更新升級。
  • 原始碼支援Qt4、Qt5、Qt6,相容所有版本。

5.3 視訊控制元件

  • 可動態新增任意多個osd標籤資訊,標籤資訊包括名字、是否可見、字號大小、文字文字、文字顏色、標籤圖片、標籤座標、標籤格式(文字、日期、時間、日期時間、圖片)、標籤位置(左上角、左下角、右上角、右下角、居中、自定義座標)。
  • 可動態新增任意多個圖形資訊,這個非常有用,比如人工智慧演演算法解析後的圖形區域資訊直接發給視訊控制元件即可。圖形資訊支援任意形狀,直接繪製在原始圖片上,採用絕對座標。
  • 圖形資訊包括名字、邊框大小、邊框顏色、背景顏色、矩形區域、路徑集合、點座標集合等。
  • 每個圖形資訊都可指定三種區域中的一種或者多種,指定了的都會繪製。
  • 內建懸浮條控制元件,懸浮條位置支援頂部、底部、左側、右側。
  • 懸浮條控制元件引數包括邊距、間距、背景透明度、背景顏色、文字顏色、按下顏色、位置、按鈕圖示程式碼集合、按鈕名稱標識集合、按鈕提示資訊集合。
  • 懸浮條控制元件一排工具按鈕可自定義,通過結構體引數設定,圖示可選圖形字型還是自定義圖片。
  • 懸浮條按鈕內部實現了錄影切換、抓拍截圖、靜音切換、關閉視訊等功能,也可以自行在原始碼中增加自己對應的功能。
  • 懸浮條按鈕對應實現了功能的按鈕,有對應圖示切換處理,比如錄影按鈕按下後會切換到正在錄影中的圖示,聲音按鈕切換後變成靜音圖示,再次切換還原。
  • 懸浮條按鈕單擊後都用名稱唯一標識作為訊號發出,可以自行關聯響應處理。
  • 懸浮條空白區域可以顯示提示資訊,預設顯示當前視訊解析度大小,可以增加影格率、碼流大小等資訊。
  • 視訊控制元件引數包括邊框大小、邊框顏色、焦點顏色、背景顏色(預設透明)、文字顏色(預設全域性文字顏色)、填充顏色(視訊外的空白處填充黑色)、背景文字、背景圖片(如果設定了圖片優先取圖片)、是否拷貝圖片、縮放顯示模式(自動調整、等比例縮放、拉伸填充)、視訊顯示模式(控制程式碼、繪製、GPU)、啟用懸浮條、懸浮條尺寸(橫向為高度、縱向為寬度)、懸浮條位置(頂部、底部、左側、右側)。

5.4 核心ffmpeg

  • 支援各種音視訊檔、本地攝像頭裝置,各種視訊流網路流。
  • 支援開始播放、暫停播放、繼續播放、停止播放、設定播放進度、倍速播放。
  • 可設定音量、靜音切換、抓拍圖片、錄影儲存。
  • 自動提取專輯資訊比如標題、藝術家、專輯、專輯封面,自動顯示專輯封面。
  • 完美支援音視訊同步和倍速播放。
  • 解碼策略支援速度優先、質量優先、均衡處理、最快速度。
  • 支援手機視訊旋轉角度顯示,比如一般手機拍攝的視訊是旋轉了90度的,解碼顯示的時候需要重新旋轉90度才是正的。
  • 自動轉換yuv420格式,比如本地攝像頭是yuyv422格式,有些視訊檔是xx格式,統一將非yuv420格式轉換,然後再進行處理。
  • 支援硬解碼dxva2、d3d11va等,效能極高尤其是大解析度比如4K視訊。
  • 視訊響應極低延遲0.2s左右,極速響應開啟視訊流0.5s左右,專門做了優化處理。
  • 硬解碼和GPU繪製組合,極低CPU佔用,比海康大華等使用者端更優。
  • 支援視訊流中的各種音訊格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支援,推薦選擇AAC相容性跨平臺性最好。
  • 視訊儲存支援yuv、h264、mp4多種格式,音訊儲存支援pcm、wav、aac多種格式。預設視訊mp4格式、音訊aac格式。
  • 支援分開儲存音訊視訊檔,也支援合併到一個mp4檔案,預設策略是無論何種音視訊檔格式儲存,最終都轉成mp4及aac格式,然後合併成音視訊一起的mp4檔案。
  • 支援本地攝像頭實時視訊顯示帶音訊輸入輸出,音視訊錄製合併到一個mp4檔案。
  • 支援H264/H265編碼(現在越來越多的監控攝像頭是H265視訊流格式)生成視訊檔,內部自動識別切換編碼格式。
  • 自動識別視訊流動態解析度改動,重新開啟視訊流。
  • 支援使用者資訊中包含特殊字元(比如使用者資訊中包含+#@等字元)的視訊流播放,內建解析跳脫處理。
  • 純qt+ffmpeg解碼,非sdl等第三方繪製播放依賴,gpu繪製採用qopenglwidget,音訊播放採用qaudiooutput。
  • 同時支援ffmpeg2、ffmpeg3、ffmpeg4、ffmpeg5版本,全部做了相容處理。如果需要支援xp需要選用ffmpeg3及以下。

到此這篇關於Qt音視訊開發之視訊檔儲存功能的實現的文章就介紹到這了,更多相關Qt視訊檔儲存內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com