一、环境介绍
操作系统: win10 64位
QT版本: QT5.12.6
编译器: MinGW 32
FFMPEG版本: 4.2.2
win32下使用FFMPEG 4.2.2库下载地址:https://download.csdn.net/download/xiaolong1126626497/12321684
码云仓库: https://gitee.com/dsxiaolong/video-reverse-player
二、功能说明
实现功能: 实现视频倒放功能,就是视频倒着播放。
说明: 因为视频本身编码特性,解码只能顺序解码。
思路: 首先得确定一个起始位置,如果默认就从视频结尾向前播放,那么这个起始位置就是视频结尾的值; 然后使用av_seek_frame 向后偏移指定的时间(比如2秒),然后在向前正常读取视频帧解码,并将数据保存到队列。 当数据读取到这个起始位置之后,就停止读取,将队列里的数据倒着取出来渲染到屏幕上,显示完毕继续重复即可。因为倒放一般只是处理图像,不处理音频,所以代码里没有对音频进行处理。
优化: (1) 目前代码里只用了一个线程读取数据、解码。如果想要倒放更加流畅,可以使用两个线程,两个队列。 一个读取,一个解码,再显示,这样就很流畅。
(2) 代码里没有对视频帧做同步处理,因为只是简单的demo,代码里只是使用了固定延时做了简单的间隔处理。后续可以使用外部时钟进行同步。
三、核心代码
完整工程源码下载: https://download.csdn.net/download/xiaolong1126626497/16522247
3.1 xxx.pro代码
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 # The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ ReverseDecodThread.cpp \ VideoFrameDisplay.cpp \ main.cpp \ widget.cpp HEADERS += \ ReverseDecodThread.h \ VideoFrameDisplay.h \ widget.h FORMS += \ widget.ui # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target win32 { message('运行win32版本') INCLUDEPATH+=C:/FFMPEG/ffmpeg_x86_4.2.2/include LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/av* LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/sw* LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/pos* }
3.2 ReverseDecodThread.cpp
//指定文件的编码为UTF-8 #pragma execution_character_set("utf-8") #include "ReverseDecodThread.h" ReverseDecodThread::ReverseDecodThread() { //注册解码器 av_register_all(); //分配上下文 format_ctx= avformat_alloc_context(); //清空队列 video_pack.clear(); qDebug() << "FFMPEG版本信息:" << av_version_info(); } ReverseDecodThread::~ReverseDecodThread() { FreeRAM(); if(format_ctx) { avformat_close_input(&format_ctx);//释放解封装器的空间,以防空间被快速消耗完 avformat_free_context(format_ctx); } } void ReverseDecodThread::FreeRAM() { if(SRC_VIDEO_pFrame) av_frame_free(&SRC_VIDEO_pFrame); if(RGB24_pFrame) av_frame_free(&RGB24_pFrame); if(img_convert_ctx)sws_freeContext(img_convert_ctx); if(out_buffer_rgb)av_free(out_buffer_rgb); SRC_VIDEO_pFrame=nullptr; RGB24_pFrame=nullptr; img_convert_ctx=nullptr; out_buffer_rgb=nullptr; } /* 工程: FFMPE_ReversePlay 日期: 2021-04-06 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载媒体文件 */ int ReverseDecodThread::LoadVideoFile(QString media) { //释放空间 FreeRAM(); //打开媒体文件 strncpy(m_MediaFile, media.toUtf8().data(), sizeof(m_MediaFile)); if(avformat_open_input(&format_ctx, m_MediaFile, nullptr, nullptr) != 0) { LogSend(tr("无法打开视频文件: %1").arg(m_MediaFile)); return -1; } //读取媒体文件的数据包以获取流信息 if(avformat_find_stream_info(format_ctx, nullptr) < 0) { LogSend(tr("无法获取流信息.\n")); return -1; } LogSend(tr("视频中流的数量: %1\n").arg(format_ctx->nb_streams)); for(int i = 0; i < format_ctx->nb_streams; ++i) { const AVStream* stream = format_ctx->streams[i]; if(stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { //查找解码器 AVCodec *video_pCodec=avcodec_find_decoder(stream->codecpar->codec_id); //打开解码器 if(avcodec_open2(stream->codec,video_pCodec,nullptr)!=0) { LogSend(tr("解码器打开失败.\n")); return -1; } video_stream_index = i; //得到视频帧的宽高 video_width=stream->codecpar->width; video_height=stream->codecpar->height; LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg( stream->codecpar->width).arg(stream->codecpar->height).arg(stream->codecpar->format)); } } if (video_stream_index == -1) { LogSend("没有检测到视频流.\n"); return -1; } AVRational frameRate = format_ctx->streams[video_stream_index]->avg_frame_rate; /*设置视频转码器*/ SRC_VIDEO_pFrame = av_frame_alloc(); RGB24_pFrame = av_frame_alloc();// 存放解码后YUV数据的缓冲区 //将解码后的YUV数据转换成RGB24 img_convert_ctx = sws_getContext(video_width, video_height, format_ctx->streams[video_stream_index]->codec->pix_fmt,video_width, video_height, AV_PIX_FMT_RGB24, SWS_BICUBIC, nullptr, nullptr, nullptr); //计算RGB图像所占字节大小 int numBytes=avpicture_get_size(AV_PIX_FMT_RGB24,video_width,video_height); //申请空间存放RGB图像数据 out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); // avpicture_fill函数将ptr指向的数据填充到picture内,但并没有拷贝,只是将picture结构内的data指针指向了ptr的数据 avpicture_fill((AVPicture *) RGB24_pFrame, out_buffer_rgb, AV_PIX_FMT_RGB24, video_width, video_height); qDebug()<<"format_ctx->duration:"<<format_ctx->duration; } void ReverseDecodThread::SetSate(int run) { m_run = run; } int ReverseDecodThread::GetSate() { return m_run; } //跳转视频帧 void ReverseDecodThread::SetSeekPos(qint64 pos) { is_CurrentSeekPos = 1; m_n64CurrentSeekPos = pos; m_run=1; //运行状态 } void ReverseDecodThread::PausePlay() { m_run = 2; } void ReverseDecodThread::StopPlay() { m_run = 0; } void ReverseDecodThread::LogSend(QString text) { qDebug() << text; } //线程执行起点 void ReverseDecodThread::run() { LogSend("开始播放视频.\n"); StartPlay(); } //解码数据包 int ReverseDecodThread::DecodDataPack() { if(video_pack.size()>0) { for (int i=video_pack.size()-1;i>=0;i--) { //通知界面更新 VideoDataOutput(video_pack.at(i).image.copy()); //时间信号 sig_getCurrentTime(video_pack.at(i).video_clock, format_ctx->duration *1.0 / AV_TIME_BASE); QThread::msleep(40); } video_pack.clear(); } return 0; } //播放视频 int ReverseDecodThread::StartPlay() { //默认从视频结尾开始播放 m_n64CurrentSeekPos=format_ctx->duration *1.0 / AV_TIME_BASE; m_endSeekPos=m_n64CurrentSeekPos; //向后偏移2秒 m_n64CurrentSeekPos-=2; //偏移到指定位置再开始解码 AVSEEK_FLAG_BACKWARD 向后找最近的关键帧 av_seek_frame(format_ctx, -1, m_n64CurrentSeekPos* AV_TIME_BASE, AVSEEK_FLAG_BACKWARD); seek_state=1; //表示视频加载成功 while(m_run) { if(m_run == 2) { msleep(100); //暂停播放 continue; } if (is_CurrentSeekPos) { m_endSeekPos=m_n64CurrentSeekPos; is_CurrentSeekPos = 0; //偏移到指定位置再开始解码 AVSEEK_FLAG_BACKWARD 向后找最近的关键帧 av_seek_frame(format_ctx, -1, m_n64CurrentSeekPos* AV_TIME_BASE, AVSEEK_FLAG_BACKWARD); qDebug()<<"跳转的位置:"<<m_n64CurrentSeekPos; //清空队列 video_pack.clear(); } double video_clock; AVPacket pkt; //读取一帧数据 if(av_read_frame(format_ctx, &pkt) < 0) { //解码数据包 DecodDataPack(); m_run=2; //设置为暂停状态 qDebug()<<"数据读取完毕."; continue; } if(pkt.stream_index == video_stream_index) { qDebug()<<"pkt.pts:"<<pkt.pts; //当前时间 video_clock = av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts; //说明之前偏移过 if(seek_state) { m_n64CurrentSeekPos=video_clock; seek_state=0; } //解码视频 frame //发送视频帧 if ( avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt) != 0) { av_packet_unref(&pkt);//不成功就释放这个pkt continue; } //接受后对视频帧进行解码 if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) != 0) { av_packet_unref(&pkt);//不成功就释放这个pkt continue; } //转格式 sws_scale(img_convert_ctx, (uint8_t const **) SRC_VIDEO_pFrame->data, SRC_VIDEO_pFrame->linesize, 0, video_height, RGB24_pFrame->data, RGB24_pFrame->linesize); //释放包 av_packet_unref(&pkt); //加载图片数据 QImage image(out_buffer_rgb,video_width,video_height,QImage::Format_RGB888); struct IMAGE_FRAME image_frame; image_frame.image=image.copy(); image_frame.video_clock=video_clock; //添加到队列 video_pack.append(image_frame); //解码到结尾 if(video_clock>=m_endSeekPos-0.5) { //读取到最开头的一帧数据 if (video_pack.size() == 1) { //解码数据包 DecodDataPack(); m_run = 2; //设置为暂停状态 continue; //回到循环头继续 } //解码数据包 DecodDataPack(); m_endSeekPos=m_n64CurrentSeekPos; //向后偏移2秒 m_n64CurrentSeekPos-=2; //偏移到指定位置再开始解码 AVSEEK_FLAG_BACKWARD 向后找最近的关键帧 av_seek_frame(format_ctx, -1, m_n64CurrentSeekPos* AV_TIME_BASE, AVSEEK_FLAG_BACKWARD); seek_state=1; } } } LogSend("视频音频解码播放器的线程退出成功.\n"); return 0; }
3.3 ReverseDecodThread.h
#ifndef VIDEO_PLAY_H #define VIDEO_PLAY_H #include <QThread> #include <qdebug.h> #include <QImage> extern "C" { #include <libavutil/opt.h> #include <libavutil/mem.h> #include <libavutil/fifo.h> #include <libavutil/pixfmt.h> #include <libavutil/log.h> #include <libavutil/opt.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> #include <libavfilter/avfilter.h> #include <libavfilter/buffersrc.h> #include <libavfilter/buffersink.h> } struct IMAGE_FRAME { QImage image; double video_clock; }; //视频音频解码线程 class ReverseDecodThread: public QThread { Q_OBJECT public: //构造函数 ReverseDecodThread(); ~ReverseDecodThread(); char m_MediaFile[1024]; int m_run; //1表示运行 0表示停止 2表示暂停 double m_n64CurrentSeekPos = 0; //当前seek位置 double m_endSeekPos=0; //结束位置 bool is_CurrentSeekPos = 0; //1需要跳转 0不需要 void SetSate(int run); int GetSate(); void SetSeekPos(qint64 pos); void PausePlay(); void StopPlay(); void LogSend(QString text); //加载视频文件 int LoadVideoFile(QString media); //释放内存 void FreeRAM(); //解码视频帧 int DecodDataPack(); protected: void run(); int StartPlay(); signals: void sig_getCurrentTime(double Sec, double total_Sec); void VideoDataOutput(QImage); //输出信号 private: int video_width=0; int video_height=0; QList <struct IMAGE_FRAME> video_pack; AVFormatContext *format_ctx=nullptr; int video_stream_index = -1; AVFrame *RGB24_pFrame = nullptr; AVFrame *SRC_VIDEO_pFrame= nullptr; uint8_t *out_buffer_rgb= nullptr; struct SwsContext *img_convert_ctx=nullptr; //用于解码后的视频格式转换 bool seek_state=0; //偏移状态 }; #endif // VIDEO_PLAY_H
3.4 效果图