音視頻/FFmpeg #Qt
Qt-FFmpeg開發-打開本地攝像頭錄制視頻【軟解碼+ OpenGL顯示YUV】
目錄- 音視頻/FFmpeg #Qt
- Qt-FFmpeg開發-打開本地攝像頭錄制視頻【軟解碼+ OpenGL顯示YUV】
- 1、概述
- 2、實作效果
- 3、FFmpeg錄制視頻編碼流程
- 4、主要代碼
- 5、完整源代碼
| 更多精彩內容 |
|---|
| ??個人內容分類匯總 ?? |
| ??音視頻開發 ?? |
1、概述
- 最近研究了一下FFmpeg開發,功能實在是太強大了,網上ffmpeg3、4的文章還是很多的,但是學習嘛,最新的還是不能放過,就選了一個最新的ffmpeg n5.1.2版本,和3、4版本api變化還是挺大的;
- 在這個Demo里主要使用Qt + FFmpeg開發一個攝像頭【錄像機】,這里主要使用的是【軟解碼】,需要使用硬解碼的可以看之前的文章;
- 在之前的文章中使用了QPainter進行繪制顯示,也講了使用OpenGL顯示RGB、YUV影像方式;
- 由于FFmpeg解碼得到的像素格式為YUVJ422P,將YUVJ422P轉換為RGB或者YUV420p都很麻煩,并且會消耗CPU資源,所以這里直接使用OpenGL顯示YUVJ422P影像,(將YUVJ422P轉RGB的步驟放到了GPU中進行),
- 關于打開攝像頭部分請看上一章,整理不重復說明,這里主要講述錄像功能,
開發環境說明
- 系統:Windows10、Ubuntu20.04
- Qt版本:V5.12.5
- 編譯器:MSVC2017-64、GCC/G++64
- FFmpeg版本:n5.1.2
- 官方下載
- 我使用的庫
2、實作效果
- 使用ffmpeg音視頻庫【軟解碼】打開本地攝像頭【錄制視頻】保存到本地;
- 采用【OpenGL顯示YUV】影像,支持自適應視窗縮放,支持使用QOpenGLWidget、QOpenGLWindow顯示;
- 將YUV轉RGB的步驟由CPU轉換改為使用GPU轉換,降低CPU占用率;
- 支持Windows、Linux打開本地攝像頭;
- 支持使用【靜態幀率】、【動態幀率】錄制視頻;
- 視頻解碼、執行緒控制、顯示各部分功能分離,低耦合度,
- 采用最新的5.1.2版本ffmpeg庫進行開發,超詳細注釋資訊,將所有踩過的坑、解決辦法、注意事項都得很寫清楚,

- 使用CPU軟解碼 + OpenGL繪制 + CPU軟編碼錄制

3、FFmpeg錄制視頻編碼流程
- 白色部分主要為創建、設定資訊步驟,藍色部分主要為寫入資料步驟,

4、主要代碼
-
啥也不說了,直接上代碼,一切有注釋
-
videosave.h檔案
/****************************************************************************** * @檔案名 videosave.h * @功能 將視頻編碼后保存到檔案中 * * @開發者 mhf * @郵箱 [email protected] * @時間 2022/11/29 * @備注 *****************************************************************************/ #ifndef VIDEOSAVE_H #define VIDEOSAVE_H #include <QString> #include <qmutex.h> struct AVCodecParameters; struct AVFormatContext; struct AVCodecContext; struct AVStream; struct AVFrame; struct AVPacket; struct AVOutputFormat; class VideoSave { public: VideoSave(); ~VideoSave(); bool open(AVStream *inStream, const QString& fileName); void write(AVFrame* frame); void close(); private: void showError(int err); private: AVFormatContext* m_formatContext = nullptr; AVCodecContext * m_codecContext = nullptr; // 編碼器背景關系 AVStream * m_videoStream = nullptr; AVPacket * m_packet = nullptr; // 資料包 int m_index = 0; bool m_writeHeader = false; // 是否寫入頭 QMutex m_mutex; }; #endif // VIDEOSAVE_H -
videosave.cpp檔案
#include "videosave.h" #include <QDebug> extern "C" { // 用C規則編譯指定的代碼 #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libavutil/avutil.h" #include "libswscale/swscale.h" #include "libavutil/imgutils.h" #include "libavdevice/avdevice.h" } #define ERROR_LEN 1024 // 例外資訊陣列長度 #define PRINT_LOG 1 #define USE_H264 0 // 使用H264編碼器 VideoSave::VideoSave() { } VideoSave::~VideoSave() { close(); } /** * @brief 顯示ffmpeg函式呼叫例外資訊 * @param err */ void VideoSave::showError(int err) { #if PRINT_LOG static char m_error[ERROR_LEN]; // 保存例外資訊 memset(m_error, 0, ERROR_LEN); // 將陣列置零 av_strerror(err, m_error, ERROR_LEN); qWarning() << "VideoSave Error:" << m_error; #else Q_UNUSED(err) #endif } bool VideoSave::open(AVStream *inStream, const QString &fileName) { if(!inStream || fileName.isEmpty()) return false; // 通過輸出檔案名為輸出格式分配AVFormatContext, #if USE_H264 int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "h264", fileName.toStdString().data()); #else /** * 攝像頭打開使用的是mjpeg編碼器; * MJPEG壓縮技術可以獲取清晰度很高的視頻影像,可以【動態調整幀率】適合保存攝像頭視頻、解析度,但由于沒有考慮到幀間變化,造成大量冗余資訊被重復存盤,因此單幀視頻的占用空間較大; * 如果采用其它編碼器,由于攝像頭曝光時間長度不一定,所以錄像時幀率一直在變,編碼器指定固定幀率會導致視頻一會快一會慢,效果很不好,適用于錄制固定幀率的視頻(當然其它編碼器應該是有處理辦法,不過我還不清楚); */ QString strName = avcodec_find_encoder(inStream->codecpar->codec_id)->name; // 獲取編碼器名稱 int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, strName.toStdString().data(), fileName.toStdString().data()); // 這里使用和解碼一樣的編碼器,防止保存的影像顏色出問題 #endif if(ret < 0) { close(); showError(ret); return false; } // 創建并初始化AVIOContext以訪問url所指示的資源, ret = avio_open(&m_formatContext->pb, fileName.toStdString().data(), AVIO_FLAG_WRITE); if(ret < 0) { close(); showError(ret); return false; } // 查詢編碼器 const AVCodec* codec = avcodec_find_encoder(m_formatContext->oformat->video_codec); if(!codec) { close(); showError(AVERROR(ENOMEM)); return false; } // 分配AVCodecContext并將其欄位設定為默認值, m_codecContext = avcodec_alloc_context3(codec); if(!m_codecContext) { close(); showError(AVERROR(ENOMEM)); return false; } // 設定編碼器背景關系引數 m_codecContext->width = inStream->codecpar->width; // 圖片寬度/高度 m_codecContext->height = inStream->codecpar->height; #if USE_H264 m_codecContext->pix_fmt = AV_PIX_FMT_YUV420P; #else m_codecContext->pix_fmt = AVPixelFormat(inStream->codecpar->format); // 像素格式,也可以使用codec->pix_fmts[0]或AV_PIX_FMT_YUVJ422P(【注意】攝像頭解碼的影像格式為yuvj422p,如果這里不一樣可能保存會出問題,或者后面進行格式轉換) #endif m_codecContext->time_base = {1, 10}; //設定時間基,20為分母,1為分子,表示以1/20秒時間間隔播放一幀影像 m_codecContext->framerate = {10, 1}; m_codecContext->bit_rate = 4000000; // 目標的碼率,即采樣的碼率;顯然,采樣碼率越大,視頻大小越大,畫質越高 m_codecContext->gop_size = 10; // I幀間隔 m_codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // m_codecContext->max_b_frames = 1; // 非B幀之間的最大B幀數(有些格式不支持) // m_codecContext->qmin = 1; // m_codecContext->qmax = 5; // m_codecContext->colorspace = AVCOL_SPC_BT470BG; // m_codecContext->color_range = AVCOL_RANGE_JPEG; // m_codecContext->color_primaries = AVCOL_PRI_BT709; // m_codecContext->bits_per_coded_sample = 24; // m_codecContext->bits_per_raw_sample = 8; // av_opt_set(m_codecContext->priv_data, "preset", "placebo", 0); // qDebug() << m_codecContext->pix_fmt; // 打開編碼器 ret = avcodec_open2(m_codecContext, nullptr, nullptr); #if USE_H264 ret = avcodec_open2(m_codecContext, codec, nullptr); // 使用h264時第一次打不開,第二次可以打卡,不知道什么原因 #endif if(ret < 0) { close(); showError(ret); return false; } // 向媒體檔案添加新流 m_videoStream = avformat_new_stream(m_formatContext, nullptr); if(!m_videoStream) { close(); showError(AVERROR(ENOMEM)); return false; } //拷貝一些引數,給codecpar賦值 ret = avcodec_parameters_from_context(m_videoStream->codecpar,m_codecContext); if(ret < 0) { close(); showError(ret); return false; } // 寫入檔案頭 ret = avformat_write_header(m_formatContext, nullptr); if(ret < 0) { close(); showError(ret); return false; } m_writeHeader = true; // 分配一個AVPacket m_packet = av_packet_alloc(); if(!m_packet) { close(); showError(AVERROR(ENOMEM)); return false; } qDebug() << "開始錄制視頻!"; return true; } /** * @brief 寫入資料 * @param frame */ void VideoSave::write(AVFrame *frame) { QMutexLocker locker(&m_mutex); if(!m_packet) { return; } if(frame) { frame->pts = m_index; // 注意:每一幀視頻顯示時間從0遞增,否則錄制的視頻顯示/時長會不對 m_index++; } // 將影像傳入編碼器 avcodec_send_frame(m_codecContext, frame); // 回圈讀取所有編碼完的幀 while (true) { // 從編碼器中讀取影像幀 int ret = avcodec_receive_packet(m_codecContext, m_packet); if(ret < 0) { break; } // 將資料包中的有效時間欄位(時間戳/持續時間)從一個時基轉換為 輸出流的時間 av_packet_rescale_ts(m_packet, m_codecContext->time_base, m_videoStream->time_base); av_write_frame(m_formatContext, m_packet); // 將資料包寫入輸出媒體檔案 av_packet_unref(m_packet); } } /** * @brief 關閉保存資料 */ void VideoSave::close() { write(nullptr); // 傳入空幀,讀取所有編碼資料 QMutexLocker locker(&m_mutex); // 如果不加鎖可能在點擊關閉時,write函式正在寫入資料,導致崩潰 if(m_formatContext) { // 寫入檔案尾 if(m_writeHeader) { m_writeHeader = false; int ret = av_write_trailer(m_formatContext); if(ret < 0) { showError(ret); return; } } int ret = avio_close(m_formatContext->pb); if(ret < 0) { showError(ret); return; } avformat_free_context(m_formatContext); m_formatContext = nullptr; } // 釋放編解碼器背景關系并置空 if(m_codecContext) { avcodec_free_context(&m_codecContext); } if(m_packet) { av_packet_free(&m_packet); qDebug() << "停止錄制視頻!"; } }
5、完整源代碼
- github
- gitee
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/549530.html
標籤:其他
