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文件
- 和之前的视频播放器
cpp
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秒,即10ms
8, true);
frame_id++;
qDebug()<<QString("当前转换第 %1 帧").arg(frame_id);
emit sig_SendFrameNum(frame_id);
-
- 创建新的
FFmpegVideo
类和新的处理线程,避免与播放线程冲突
- 创建新的
cpp
m_FFmpegProcessing = new FFmpegVideo();
m_ProcessingThread = new QThread(this);
m_FFmpegProcessing->moveToThread(m_ProcessingThread); // 移动到线程中
-
- 创建非模态的进度条,发送
sig_SendFrameNum
帧数信号设置进度条进度 同时判断是否点击了进度条的按钮 (稳妥起见此连接设置为Qt::BlockingQueuedConnection
- 确定同步执行对m_stopProcessing
及时赋值)
- 创建非模态的进度条,发送
cpp
// 进度条
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)); // 考虑是否移换种方式显示进度条进度... 不使用帧数
cpp
// 进度条绑定
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
- 子线程中会判断
cpp
while(this->m_stopProcessing == false)
cpp
GifEnd(&writer); // 取消之后是否需要保存不完整的gif? 暂时保存
使用事件循环,信号,stop变量,sleep阻塞,QWaitCondition+QMutex条件变量,退出子线程工作:https://blog.csdn.net/u012999461/article/details/127204493
-
- 进度条在函数中new的,子线程结束之后需释放
deleteLater
。 还有一些小问题... 比如点两次另存为gif,可以同时弹出两个进度条等等 - 进度条没必要每次都new... 后续继续改进
- 进度条在函数中new的,子线程结束之后需释放
cpp
// 开始转换 在这里连接需注意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()
cpp
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秒,即10ms
8, 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, ///todo
nullptr,
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秒,即10ms
8, 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()
cpp
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