Qt 基于FFmpeg的視頻轉換器 - 轉GIF動圖
- 引言
- 一、設計思路
- 二、核心源碼
- 三、參考鏈接
引言
gif
格式的動圖可以通過連續播放一系列圖像或視頻片段來展示動態效果,使信息更加生動形象,可以很方便的嵌入到網頁或者ppt中
。上圖展示了視頻的前幾幀轉為gif動圖的效果 (轉了7%直接取消了)。
之前寫過一個基于python的 MP4視頻轉GIF動圖,速度略慢且不容易打包 (體積很大),故基于c++寫一個小程序,方便日常使用. (這里推薦幾個gif生成的小工具 - GifCam
、ScreenGif.exe
、LICEcap.exe
等等 or 直接使用ffmpeg
提供的小工具)
- 本文思路:基于
FFmpeg
進行視頻的讀取解碼成一張張圖片,調用gif.h
將圖片寫入gif
gif-h官方git地址:https://github.com/charlietangora/gif-h
一、設計思路
可參考之前的博客:Qt 基于FFmpeg的視頻播放器 - QtFFmpegPlayer
-
- 和之前的視頻播放器
play()
函數類似,實現savetoGif()
函數,將視頻的一幀解碼成圖片后,立即寫入gif文件
- 和之前的視頻播放器
GifWriteFrame(&writer, image.bits(),static_cast<uint32_t>(avcodec_context->width),static_cast<uint32_t>(avcodec_context->height),static_cast<uint32_t>(100/this->m_fps), // 單位是1/100秒,即10ms8, true);frame_id++;qDebug()<<QString("當前轉換第 %1 幀").arg(frame_id);emit sig_SendFrameNum(frame_id);
-
- 創建新的
FFmpegVideo
類和新的處理線程,避免與播放線程沖突
- 創建新的
m_FFmpegProcessing = new FFmpegVideo();
m_ProcessingThread = new QThread(this);
m_FFmpegProcessing->moveToThread(m_ProcessingThread); // 移動到線程中
-
- 創建非模態的進度條,發送
sig_SendFrameNum
幀數信號設置進度條進度 同時判斷是否點擊了進度條的按鈕 (穩妥起見此連接設置為Qt::BlockingQueuedConnection
- 確定同步執行對m_stopProcessing
及時賦值)
- 創建非模態的進度條,發送
// 進度條progressDialog = new QProgressDialog();progressDialog->setMinimumWidth(300); // 設置最小寬度progressDialog->setWindowModality(Qt::NonModal); // 非模態,其它窗口正常交互 Qt::WindowModal 模態progressDialog->setMinimumDuration(0); // 等待0秒后顯示progressDialog->setWindowTitle(tr("進度條框")); // 標題名progressDialog->setLabelText(tr("正在轉換")); // 標簽的progressDialog->setCancelButtonText(tr("放棄")); // 取消按鈕progressDialog->setRange(0, static_cast<int>(m_FFmpegProcessing->m_frame_num)); // 考慮是否移換種方式顯示進度條進度... 不使用幀數
// 進度條綁定
connect(m_FFmpegProcessing, &FFmpegVideo::sig_SendFrameNum, this, [&](int num){if(progressDialog->wasCanceled()){ // 彈窗的取消按鈕m_FFmpegProcessing->m_stopProcessing = true;return;}progressDialog->setValue(num);}, Qt::BlockingQueuedConnection); // 發送信號后,先執行此內容 再繼續執行線程,保證線程可以及時推出
使用
lambda
表達式接收信號,需要注意其默認參數… 建議寫完整防止奇奇怪怪的問題
Qt使用connect連接信號與lambda表達式需要注意:https://blog.csdn.net/qq_17769915/article/details/132609165
qt 如何使用 lamda 表達式接收線程中發射的數據,并在里面更新 UI ?https://www.cnblogs.com/cheungxiongwei/p/10895172.html
-
- 子線程中會判斷
m_stopProcessing
- 是否點擊了進度條的退出按鈕. 如果點擊了按鈕,最后也會執行GifEnd
生成一個不完整的gif
- 子線程中會判斷
while(this->m_stopProcessing == false)
GifEnd(&writer); // 取消之后是否需要保存不完整的gif? 暫時保存
使用事件循環,信號,stop變量,sleep阻塞,QWaitCondition+QMutex條件變量,退出子線程工作:https://blog.csdn.net/u012999461/article/details/127204493
-
- 進度條在函數中new的,子線程結束之后需釋放
deleteLater
。 還有一些小問題… 比如點兩次另存為gif,可以同時彈出兩個進度條等等 - 進度條沒必要每次都new… 后續繼續改進
- 進度條在函數中new的,子線程結束之后需釋放
// 開始轉換 在這里連接需注意Qt::UniqueConnection 使得連接唯一
connect(m_ProcessingThread, SIGNAL(started()), m_FFmpegProcessing, SLOT(savetoGif()), Qt::UniqueConnection);
connect(m_ProcessingThread, &QThread::finished, progressDialog, &QProgressDialog::deleteLater, Qt::UniqueConnection);
m_ProcessingThread->start();
m_ProcessingThread->quit();
二、核心源碼
其他源碼可參考我之前的博客:Qt 基于FFmpeg的視頻播放器 - QtFFmpegPlayer
FFmpegVideo::savetoGif()
void FFmpegVideo::savetoGif()
{qDebug()<<"savetoGif";//avformat_seek_file()GifWriter writer = {};GifBegin(&writer, this->m_outfilename.toStdString().c_str(),static_cast<uint32_t>(avcodec_context->width),static_cast<uint32_t>(avcodec_context->height),static_cast<uint32_t>(100/this->m_fps), // 單位是1/100秒,即10ms8, true );// 初始化臨時變量AVPacket* av_packet = static_cast<AVPacket*>(av_malloc(sizeof(AVPacket)));AVFrame *pFramein = av_frame_alloc(); //輸入和輸出的幀數據AVFrame *pFrameRGB = av_frame_alloc();uint8_t * pOutbuffer = static_cast<uint8_t *>(av_malloc( //緩沖區分配內存static_cast<quint64>(av_image_get_buffer_size(AV_PIX_FMT_RGBA,avcodec_context->width,avcodec_context->height,1))));// 初始化緩沖區av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize,pOutbuffer,AV_PIX_FMT_RGB32,avcodec_context->width, avcodec_context->height, 1);// 格式轉換SwsContext* pSwsContext = sws_getContext(avcodec_context->width, // 輸入寬avcodec_context->height, // 輸入高avcodec_context->pix_fmt, // 輸入格式avcodec_context->width, // 輸出寬avcodec_context->height, // 輸出高AV_PIX_FMT_RGBA, // 輸出格式SWS_BICUBIC, ///todonullptr,nullptr,nullptr);int ret=0;int frame_id = 0;this->m_stopProcessing = false;// 開始循環while(this->m_stopProcessing == false){if (av_read_frame(avformat_context, av_packet) >= 0){if (av_packet->stream_index == av_stream_index){avcodec_send_packet(avcodec_context, av_packet); // 解碼ret = avcodec_receive_frame(avcodec_context, pFramein); // 獲取解碼輸出if (ret == 0){sws_scale(pSwsContext, //圖片格式的轉換static_cast<const uint8_t* const*>(pFramein->data),pFramein->linesize, 0, avcodec_context->height,pFrameRGB->data, pFrameRGB->linesize);QImage *tmpImg = new QImage(static_cast<uchar *>(pOutbuffer),avcodec_context->width,avcodec_context->height,QImage::Format_RGBA8888);QImage image = tmpImg->copy();GifWriteFrame(&writer, image.bits(),static_cast<uint32_t>(avcodec_context->width),static_cast<uint32_t>(avcodec_context->height),static_cast<uint32_t>(100/this->m_fps), // 單位是1/100秒,即10ms8, true);frame_id++;qDebug()<<QString("當前轉換第 %1 幀").arg(frame_id);emit sig_SendFrameNum(frame_id);//break;}}}}GifEnd(&writer); // 取消之后是否需要保存不完整的gif? 暫時保存av_packet_unref(av_packet);
}
-
MainWindow::saveVideo()
void MainWindow::saveVideo()
{if(!m_FFmpegVideo){return;}m_FFmpegProcessing->loadVideoFile(m_FFmpegVideo->m_filename); // 讀取視頻QFileInfo fileInfo(m_FFmpegProcessing->m_filename);QString filePath = QFileDialog::getSaveFileName(this, QObject::tr("Open File"),fileInfo.completeBaseName() + ".gif",QObject::tr("gif (*.gif) ;; All Files (*)"));m_FFmpegProcessing->m_outfilename = filePath; // 輸出文件fileInfo.setFile(filePath);// 轉GIF ------------int ret = fileInfo.suffix().compare(QString("gif"), Qt::CaseInsensitive);// 進度條progressDialog = new QProgressDialog();progressDialog->setMinimumWidth(300); // 設置最小寬度progressDialog->setWindowModality(Qt::NonModal); // 非模態,其它窗口正常交互 Qt::WindowModal 模態progressDialog->setMinimumDuration(0); // 等待0秒后顯示progressDialog->setWindowTitle(tr("進度條框")); // 標題名progressDialog->setLabelText(tr("正在轉換")); // 標簽的progressDialog->setCancelButtonText(tr("放棄")); // 取消按鈕progressDialog->setRange(0, static_cast<int>(m_FFmpegProcessing->m_frame_num)); // 考慮是否移換種方式顯示進度條進度... 不使用幀數// 轉換if(ret == 0){// 進度條綁定connect(m_FFmpegProcessing, &FFmpegVideo::sig_SendFrameNum, this, [&](int num){if(progressDialog->wasCanceled()){ // 彈窗的取消按鈕m_FFmpegProcessing->m_stopProcessing = true;return;}progressDialog->setValue(num);}, Qt::BlockingQueuedConnection); // 發送信號后,先執行此內容 再繼續執行線程,保證線程可以及時推出// 開始轉換 在這里連接需注意Qt::UniqueConnection 使得連接唯一connect(m_ProcessingThread, SIGNAL(started()), m_FFmpegProcessing, SLOT(savetoGif()), Qt::UniqueConnection);connect(m_ProcessingThread, &QThread::finished, progressDialog, &QProgressDialog::deleteLater, Qt::UniqueConnection);m_ProcessingThread->start();m_ProcessingThread->quit();}
}
三、參考鏈接
-
- 直接調用工具:
用ffmpeg提供的工具將視頻轉成gif動圖:https://blog.csdn.net/xindoo/article/details/127603896
Android錄屏并利用FFmpeg轉換成gif:https://blog.csdn.net/MingHuang2017/article/details/79186527
-
- 代碼實現:
Qt項目中,實現屏幕截圖并生成gif的詳細示例:https://www.zhihu.com/tardis/bd/art/194303756
Qt編寫自定義控件35-GIF錄屏控件:https://developer.aliyun.com/article/712842
ffmpeg生成gif動圖:https://www.jianshu.com/p/d9652fc2e3fd
FFmpeg進階: 截取視頻生成gif動圖:https://zhuanlan.zhihu.com/p/628705382