百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

QT应用编程: 基于FFMPEG完成视频倒放功能

liebian365 2024-10-17 14:01 7 浏览 0 评论

一、环境介绍

操作系统: 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,代码里只是使用了固定延时做了简单的间隔处理。后续可以使用外部时钟进行同步。

三、核心代码

完整工程源码下载: download.csdn.net/download/xi…

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*
}

【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】

点击这里:「链接」

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 效果图

【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】

点击这里:「链接」
作者:DS小龙哥
链接:https://juejin.cn/post/6985337393431707656

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: