فهرست منبع

V0.3.6
1、添加了VideoPlayer工程

Apple 5 ماه پیش
والد
کامیت
2a46fd6904

+ 4 - 3
CMakeLists.txt

@@ -116,6 +116,7 @@ find_package(Qt5 COMPONENTS
 )
 
 include(${CMAKE_SOURCE_DIR}/External/Libraries/Libraries.cmake)
+# include(${CMAKE_SOURCE_DIR}/External_Linux/Library_EX.cmake)
 
 
 #=========================================================
@@ -144,7 +145,7 @@ file(GLOB GLOBAL_SRC
 # add_subdirectory(${CMAKE_SOURCE_DIR}/demo/threadPool)
 # add_subdirectory(${CMAKE_SOURCE_DIR}/demo/ftp)
 # add_subdirectory(${CMAKE_SOURCE_DIR}/demo/OneThread)
-add_subdirectory(${CMAKE_SOURCE_DIR}/demo/timer)
-
-
+# add_subdirectory(${CMAKE_SOURCE_DIR}/demo/timer)
+# add_subdirectory(${CMAKE_SOURCE_DIR}/demo/time)
+add_subdirectory(${CMAKE_SOURCE_DIR}/demo/VideoPlayer)
 

+ 1 - 0
External_Linux

@@ -0,0 +1 @@
+/home/Apple/Design/Library/Linux_Library

+ 86 - 0
demo/VideoPlayer/CMakeLists.txt

@@ -0,0 +1,86 @@
+cmake_minimum_required(VERSION 3.5)
+
+set(this_exe VideoPlayer)
+
+
+#包含源文件
+file(GLOB LOCAL_SRC
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.rc
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
+    ${CMAKE_CURRENT_SOURCE_DIR}/VideoPlayer/*.cpp
+
+    ${CMAKE_SOURCE_DIR}/External/common/Logs/*.cpp
+    ${CMAKE_SOURCE_DIR}/External/common/ThreadPool/*.cpp
+    
+)
+
+
+# 生成可执行程序
+
+add_executable(${this_exe}
+    # WIN32
+    ${GLOBAL_SRC}
+    ${LOCAL_SRC} 
+)
+
+# set_target_properties(${this_exe} PROPERTIES
+    
+# )
+
+
+#添加头文件
+target_include_directories(${this_exe} PRIVATE
+
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/VideoPlayer
+    ${CMAKE_SOURCE_DIR}/External/common
+    ${CMAKE_SOURCE_DIR}/External/common/FmtLog
+    ${CMAKE_SOURCE_DIR}/External/common/ThreadPool
+    ${CMAKE_SOURCE_DIR}/External/common/RingQueue
+    
+    ${CURL_INCLUDE_DIR}
+    # ${FFMPEG2_INCLUDE_DIR}
+)
+
+target_link_libraries(${this_exe} PRIVATE
+    Qt5::Widgets
+    Qt5::Core
+    Qt5::Network
+    # Qt5::Multimedia
+    # Qt5::Xml
+    # Qt5::Sql
+)
+
+target_link_libraries(${this_exe} PRIVATE 
+    fmt::fmt
+    spdlog::spdlog
+    ${CURL_LIBRARY}
+    # ${FFMPEG2_LIBRARY}
+)
+
+if(CMAKE_CXX_COMPILER_VERSION LESS 9.0)
+    target_link_libraries(${this_exe} PRIVATE
+        stdc++fs
+    )
+endif()
+
+# target_link_libraries(${this_exe} PRIVATE
+#     ${CURL_LIBRARY}
+    
+# )
+# message(STATUS "CURL_LIBRARY: ${CURL_LIBRARY}")
+
+
+# if(CMAKE_CXX_COMPILER_ID MATCHES MSVC)
+#     target_link_libraries(${this_exe} PRIVATE
+#         # debug spdlogd.lib
+#         # optimized spdlog.lib
+#     )
+# elseif(CMAKE_CXX_COMPILER_ID MATCHES GNU)
+#     target_link_libraries(${this_exe} PRIVATE
+#         # debug 
+#         # optimized ${SM_DLL}
+#     )
+# endif()

+ 704 - 0
demo/VideoPlayer/VideoPlayer/DecodeVedio.cpp

@@ -0,0 +1,704 @@
+#include "DecodeVedio.h"
+#include "spdlog/spdlog.h"
+#include "FmtLog/fmtlog.h"
+
+#include <QImage>
+#include <QThread>
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libswscale/swscale.h>
+#include <libavutil/imgutils.h>
+}
+
+
+DecodeVedio::DecodeVedio(QThread* thread, QObject* parent) : QObject(parent) , m_thread(thread)
+{
+    /* 在连接之前调用,移动到新的线程 */
+    this->moveToThread(thread);
+    m_startThread.setSingleShot(true);
+    connect(&m_startThread, &QTimer::timeout, this, &DecodeVedio::do_startDecodeVedio);
+    thread->start();
+    getHWDecoder();
+}
+
+DecodeVedio::~DecodeVedio()
+{
+    exitThread();
+    if(m_thread != nullptr)
+    {
+        if(m_thread->isRunning())
+        {
+            m_thread->quit();
+            m_thread->wait();
+        }
+    }
+    if(m_initFFmpeg)
+    {
+        freeFFmpeg();
+    }
+    if(!m_queueImage.isEmpty())
+    {
+        int size = m_queueImage.count();
+        for(int i = 0; i < size; i++)
+        {
+            auto image = m_queueImage.dequeue();
+            if (image)
+            {
+                delete image;
+            }
+            
+        }
+        
+    }
+}
+
+/* 获取硬件解码器 */
+void DecodeVedio::getHWDecoder()
+{
+    AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;
+    QStringList strTypes;
+    while( (type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE)
+    {
+        m_listHWDeviceType.append(type);
+        /* 获取硬件解码器的名称 */
+        const char* typeName = av_hwdevice_get_type_name(type);
+        if(typeName)
+        {
+            strTypes.append(QString(typeName));
+        }
+    }
+    FMTLOG_INFO_NON("支持的硬件解码器:");
+    for(auto type : strTypes)
+    {
+        FMTLOG_INFO_NON(" {}",type.toStdString());
+    }
+}
+
+/* 开始解码视频 */
+void DecodeVedio::startDecodeVedio()
+{
+    if(m_isRunning)
+    {
+        return;
+    }
+    if(!m_initFFmpeg)
+    {
+        SPDLOG_WARN("未初始化FFMPEG...");
+        return;
+    }
+    /* 开启定时器,进入槽函数,就进入了新的线程 */
+    m_startThread.start(0);
+}
+
+/* 停止解码视频,退出工作函数,线程未停止 */
+void DecodeVedio::stopDecodeVedio()
+{
+    if(!m_isRunning)
+    {
+        return;
+    }
+    
+    m_isRunning = false;
+    m_pauseDecode = false;
+    /* 唤醒阻塞住的解码线程 */
+    m_condQueueNoFull.wakeAll();
+    // /* 等待线程执行结束 */
+    while(!m_threadStopped)
+    {
+        /* 睡眠10ms */
+        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+    }
+    
+}
+
+/**
+ * @brief 设置当前播放位置,单位是微秒
+ *        这里需要去掉队列中已有的图片数目对应的时长
+ * 
+ * @param pos 
+ */
+void DecodeVedio::setCurrentPos(quint64 pos)
+{
+    
+    /* 暂停解码 */
+    m_pauseDecode = true;
+    // SPDLOG_DEBUG("setCurrentPos threadID:{}",QThread::currentThreadId());
+    while (m_decodeStatus) {
+        /* 需要唤醒阻塞住的线程 */
+        m_condQueueNoFull.wakeAll();
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+    m_isSeek = true;
+    /* 这个是int64最大的正数值 */
+    if(pos > 9223372036854775807)
+    {
+        pos = 0;
+    }
+    /* 去掉队列中已有的图片数目对应的时长,单位是us */
+    qint64 queueNumFPS = m_queueImage.count() * ( 1000.0f / m_fps ) * 1000 ;
+    qint64 target_pos = m_startPos + pos - queueNumFPS;
+    qint64 end_pos = m_startPos + m_duration;
+    /* 检查是否超过视频大小 */
+    if(target_pos > end_pos)
+    {
+        /* 跳转到最后 */
+        target_pos = end_pos;
+        // SPDLOG_DEBUG("现在的位置:{} 时长:{} 跳转到最后30帧",target_pos, m_duration);
+    }
+    if(target_pos < m_startPos)
+    {
+        target_pos = m_startPos;
+        // SPDLOG_DEBUG("现在的位置:{} 时长:{} 跳转到开始",target_pos, m_startPos);
+    }
+    m_currentPos = target_pos;
+    SPDLOG_DEBUG("设置播放位置:{}",target_pos);
+    /* 第二个参数设置为-1,表示所有流都跳转 */
+    int ret = av_seek_frame(m_pFormatContext, -1, target_pos, AVSEEK_FLAG_BACKWARD);
+    /* 清空缓存 */
+    avcodec_flush_buffers(m_pCodecCtx);
+    if(ret < 0)
+    {
+        SPDLOG_WARN("跳转失败...");
+    }
+    /* 清空队列 */
+    m_mutexQueue.lock();
+    int size = m_queueImage.count();
+    for(int i = 0; i < size; i++)
+    {
+        auto image = m_queueImage.dequeue();
+        if (image)
+        {
+            delete image;
+        }
+            
+    }
+    m_mutexQueue.unlock();
+    m_condQueueNoFull.wakeAll();
+    SPDLOG_INFO("跳转完成 , queue count {}", m_queueImage.count());
+    /* 继续解码 */
+    m_pauseDecode = false;
+    // SPDLOG_DEBUG("继续解码... {}", m_pauseDecode.load());
+}
+
+/* 获取当前播放位置 */
+qint64 DecodeVedio::getCurrentPos()
+{
+    
+    return m_currentPos - m_startPos;
+}
+
+/* 获取视频时长 */
+qint64 DecodeVedio::getDuration()
+{
+    return m_duration;
+
+}
+
+/* 唤醒队列不满条件变量 */
+void DecodeVedio::wakeUpCondQueueNoEmpty()
+{
+    m_condQueueNoFull.wakeAll();    
+}
+
+/**
+ * @brief 获取一帧图像,队列为空就返回nullptr,这个函数应该是运行在UI线程中的
+ * @warning 传出这个指针后,队列就出队了,内存需要外面获取的实例释放
+ * @return QImage* 一帧图像的指针
+ */
+QImage* DecodeVedio::getOneImage()
+{
+    if(m_queueImage.count() == 0)
+    {
+        // SPDLOG_TRACE("队列为空...");
+        return nullptr;
+    }
+    // SPDLOG_TRACE("******************************** 队列中图片个数:{} ",m_queueImage.count());
+    m_mutexQueue.lock();
+    auto image = m_queueImage.dequeue();
+    m_mutexQueue.unlock();
+    /* 唤醒可能阻塞住的解码线程,队列中的图片低于20之后再唤醒 */
+    if(m_queueImage.count() < 20)
+    {
+        m_condQueueNoFull.wakeAll();
+    }
+    return image;
+}
+
+/* 获取一帧图像,直到有图像为止 */
+QImage* DecodeVedio::getOneImageUntilHave()
+{
+    QImage* image = nullptr;
+    if(m_queueImage.count() == 0)
+    {
+        m_mutexQueue.lock();
+        m_condQueueNoEmpty.wait(&m_mutexQueue);
+        image = m_queueImage.dequeue();
+        m_mutexQueue.unlock();
+    }
+
+    return image;
+}
+
+/**
+ * @brief 获取每秒的帧数
+ * 
+ * @return int 正值表示帧数,-1表示未知,-2表示HEVC
+
+ */
+int DecodeVedio::getFrameCount()
+{
+    AVFormatContext *pFormatCtx = NULL;
+    avformat_open_input(&pFormatCtx, m_fileName.toStdString().c_str(), NULL, NULL);
+    avformat_find_stream_info(pFormatCtx, NULL);
+    /* 找到视频流 */
+    int videoStreamIndex = -1;
+    for (int i = 0; i < pFormatCtx->nb_streams; i++) 
+    {
+        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
+            videoStreamIndex = i;
+            break;
+        }
+    }
+    /* 获取视频格式是hevc还是h.264 */
+    enum AVCodecID codecID = pFormatCtx->streams[videoStreamIndex]->codecpar->codec_id;
+    if (codecID == AV_CODEC_ID_HEVC) 
+    {
+        SPDLOG_DEBUG("视频格式是HEVC");
+        m_fps = -2;
+        // AVPacket packet;
+        // int frameCount = 0;
+        // double timeBase = av_q2d(pFormatCtx->streams[videoStreamIndex]->time_base);
+        // double duration = 2.0;  // Duration in seconds
+
+        // while (av_read_frame(pFormatCtx, &packet) >= 0) 
+        // {
+        //     if (packet.stream_index == videoStreamIndex) 
+        //     {
+        //         double packetTime = packet.pts * timeBase;
+        //         if (packetTime <= duration) {
+        //             frameCount++;
+        //         } else {
+        //             break;
+        //         }
+        //     }
+        //     av_packet_unref(&packet);
+        // }
+        // fps = frameCount / duration;
+    }
+    else if(codecID == AV_CODEC_ID_H264)
+    {
+        SPDLOG_DEBUG("视频格式是H.264");
+        /* 获取到分子和分母,一除就是帧率 */
+        AVRational frameRate = pFormatCtx->streams[videoStreamIndex]->avg_frame_rate;
+        m_fps = frameRate.num*1.0 / frameRate.den;
+        // SPDLOG_DEBUG("分子:{} 分母:{} 帧率:{}",frameRate.num, frameRate.den, fps);
+    }else
+    {
+        m_fps = 30;
+    }
+
+    avformat_close_input(&pFormatCtx);
+
+    return m_fps;
+}
+/**
+ * @brief 设置图像宽度和高度
+ *        注意:目前不好用,建议使用Qt的图片缩放
+ * 
+ * @param width 
+ * @param height 
+ */
+void DecodeVedio::setVideoSize(int width, int height)
+{
+    m_width = width;
+    m_height = height;
+
+    if(m_sws_ctx != nullptr)
+    {
+        /* 先暂停解码器 */
+        pauseDecode();
+        sws_freeContext(m_sws_ctx);
+    }
+    /* 初始化Sws Context,这是转换规则,转换成RGB */
+    m_sws_ctx = sws_getContext( m_pCodecCtx->width, m_pCodecCtx->height, m_pCodecCtx->pix_fmt,    /* 原图像大小和格式 */
+                                m_width, m_height, AV_PIX_FMT_RGB24,      /* 目标图像的大小和格式 */
+                                SWS_BICUBIC, 
+                                nullptr, 
+                                nullptr, 
+                                nullptr);
+}
+
+/* 初始化函数 */
+void DecodeVedio::initFFmpeg(const QString& fileName)
+{
+    if(m_initFFmpeg)
+    {
+        return;
+    }
+    m_fileName = fileName;
+    /* 清空队列 */
+    int size = m_queueImage.count();
+    for(int i = 0; i < size; i++)
+    {
+        auto image = m_queueImage.dequeue();
+        if (image)
+        {
+            delete image;
+        }
+    }
+    m_queueImage.clear();
+
+    SPDLOG_DEBUG("开始初始化FFMPEG");
+    /* 注册所有的解码器,从4.0开始就不需要这个初始化了 */
+    // av_register_all();
+
+    /* 存储文件格式信息 */
+    /* 打开文件,读取视频文件的头信息,放在第一个参数的结构体中 */
+    int ret = avformat_open_input(&m_pFormatContext, m_fileName.toStdString().c_str(), nullptr, nullptr);
+    if(ret != 0)
+    {
+        SPDLOG_WARN("打开视频文件错误,错误代码:{}",ret);
+        return;
+    }
+
+    ret = 0;
+    /* 检查视频容器内部的流信息,将流存储到了pFormatContext->streams中
+     * 这个会补充avformat_open_input()没有获取到的信息 */
+    ret = avformat_find_stream_info(m_pFormatContext, nullptr);
+    if(ret < 0)
+    {
+        SPDLOG_WARN("获取视频流错误,错误代码:{}",ret);
+        return;
+    }
+    m_duration = m_pFormatContext->duration;
+    m_currentPos = 0;
+    m_startPos = m_pFormatContext->start_time;
+    // SPDLOG_DEBUG("开始时间:{} 时长:{}",m_startPos, m_duration);
+    
+    /* 一个调试函数,将流信息输出到控制台 */
+    av_dump_format(m_pFormatContext, 0, m_fileName.toStdString().c_str(), 0);
+
+    /************ 找到视频流 ************/
+    int i = 0;
+    AVCodecParameters *pCodecCtxOrig = nullptr;
+    
+    for(i = 0;i < m_pFormatContext->nb_streams; i++)
+    {
+        if(m_pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+        {
+            m_videoStream = i;
+            break;
+        }
+    }
+    if(m_videoStream == -1)
+    {
+        SPDLOG_WARN("没有找到视频流");
+        return;
+    }
+    SPDLOG_INFO("找到视频流");
+    
+    pCodecCtxOrig = m_pFormatContext->streams[m_videoStream]->codecpar;
+    SPDLOG_INFO("获取视频流参数成功!");
+    /* 使用编码器指向流 */
+    AVCodec *pCodec = nullptr;
+    /* 找到解码器 */
+    pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
+    if(pCodec == nullptr)
+    {
+        SPDLOG_WARN("没有找到解码器");
+        return;
+    }
+    SPDLOG_TRACE("找到解码器");
+    /* 复制上下文,先分配空间,后面记得释放空间 */
+    m_pCodecCtx = avcodec_alloc_context3(pCodec);
+    SPDLOG_TRACE("分配空间成功!");
+    /* 将视频流中的编码器参数拷贝下来,这个函数不是线程安全的 */
+    if(avcodec_parameters_to_context(m_pCodecCtx, pCodecCtxOrig) != 0)
+    {
+        SPDLOG_WARN("复制上下文错误");
+        return;
+    }
+    SPDLOG_INFO("复制上下文成功!");
+    if(avcodec_open2(m_pCodecCtx, pCodec, nullptr) < 0)
+    {
+        SPDLOG_ERROR("打开解码器错误");
+        return;
+    }
+    SPDLOG_TRACE("打开编码器成功!");
+
+    /******** 读取数据(解码数据) ********/
+    m_packet = av_packet_alloc();
+    // av_init_packet(m_packet);
+    av_new_packet(m_packet, m_pCodecCtx->width * m_pCodecCtx->height);
+
+    /********* 创建两个pFrame,一个存放原始数据,一个存放转换后的RGB数据 **********/
+    m_pFrame = av_frame_alloc();
+    if(m_pFrame == nullptr)
+    {
+        SPDLOG_ERROR("创建pFrame错误");
+        return;
+    }
+    m_pFrameRGB = av_frame_alloc();
+    if(m_pFrameRGB == nullptr)
+    {
+        SPDLOG_ERROR("创建pFrameRGB错误");
+        return;
+    }
+    
+    int numBytes = 0;
+    /* 初始化pFrameRGB */
+    numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, m_pCodecCtx->width, m_pCodecCtx->height, 1);
+    m_buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
+    /* 获取视频相关信息 */
+    m_width = m_srcWidth = m_pCodecCtx->width;
+    m_height = m_srcHeight = m_pCodecCtx->height;
+    m_frameCount = m_pFormatContext->streams[m_videoStream]->nb_frames;
+    
+    /* 这个函数的实际作用是将buffer设置给pFrameRGB作为原始数据的内存区域 */
+    av_image_fill_arrays(m_pFrameRGB->data, m_pFrameRGB->linesize, m_buffer, AV_PIX_FMT_RGB24, m_pCodecCtx->width, m_pCodecCtx->height, 1);
+
+    /********** 创建一个SwsContext结构体,主要为了格式转化的时候使用 ***********/
+    
+    /* 初始化Sws Context,这是转换规则,转换成RGB */
+    m_sws_ctx = sws_getContext( m_pCodecCtx->width, m_pCodecCtx->height, m_pCodecCtx->pix_fmt,    /* 原图像大小和格式 */
+                                m_srcWidth, m_srcHeight, AV_PIX_FMT_RGB24,      /* 目标图像的大小和格式 */
+                                SWS_BILINEAR,           /* 双线性 */
+                                nullptr, 
+                                nullptr, 
+                                nullptr);
+
+    m_initFFmpeg = true;
+    SPDLOG_INFO("FFMPEG初始化完成!");
+    /* 这里不能释放,否则会出问题 */
+    // avcodec_parameters_free(&pCodecCtxOrig);
+}
+
+
+/* 取消ffmpeg初始化函数 */
+void DecodeVedio::unInitFFmpeg()
+{
+    stopDecodeVedio();
+}
+
+
+
+/* 取消ffmpeg初始化函数 */
+void DecodeVedio::freeFFmpeg()
+{
+    /************** 清理缓冲区,释放空间 *********************/
+    av_free(m_buffer);
+    m_buffer = nullptr;
+    av_free(m_pFrameRGB);
+    m_pFrameRGB = nullptr;
+    /* 释放pFrame */
+    av_free(m_pFrame);
+    m_pFrame = nullptr;
+    av_packet_free(&m_packet);
+    // sws_freeContext(m_sws_ctx);
+    /* 关闭解码器 */
+    avcodec_close(m_pCodecCtx);
+    /* 关闭文件 */
+    avformat_close_input(&m_pFormatContext);
+
+    QString m_fileName = QString();
+    int m_videoStream = -1;                         /* 记录视频流是第几个流 */
+    m_initFFmpeg = false;
+    
+}
+
+/**
+ * @brief 进入之后就会一直解码,完成一个帧就会发送新一个信号
+ * 
+ */
+void DecodeVedio::decodeVedio()
+{
+    int ret = 0;
+    int retFrame = 0;
+    int retPacket = 0;
+    m_pauseDecode = false;
+    m_decodeStatus = true;
+    while(m_isRunning)
+    {
+        /* 暂停解码 */
+        while(m_pauseDecode)
+        {
+            m_decodeStatus = false;
+            std::this_thread::sleep_for(std::chrono::milliseconds(1));
+        }
+        m_decodeStatus = true;
+        // std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
+        /* 读取一帧压缩码流,如果使用多线程解码,就从这里分发数据流的包 */
+        retPacket = av_read_frame(m_pFormatContext, m_packet);
+        // std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
+        // SPDLOG_TRACE("======================================= 读取数据包耗时(us):{}",std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count());
+        if(retPacket == AVERROR_EOF)
+        {
+            SPDLOG_INFO("读取到文件末尾...");
+            emit signal_playCompleted();
+            /* 暂停解码 */
+            m_pauseDecode = true;
+            while(m_pauseDecode)
+            {
+                m_decodeStatus = false;
+                std::this_thread::sleep_for(std::chrono::milliseconds(5));
+            }
+        }
+        else if(retPacket < 0)
+        {
+            SPDLOG_WARN("读取帧错误...");
+            break;
+        }
+        
+        /* 判断是否是视频帧 */
+        if(m_packet->stream_index == m_videoStream)
+        {
+            /* 现在的位置,转换成时间,乘上AV_TIME_BASE后,单位是微秒 */
+            auto tmpPos = m_packet->pts * av_q2d(m_pFormatContext->streams[m_videoStream]->time_base) * AV_TIME_BASE;
+            /* 判断是否在跳转状态,和设置的位置相距较远就跳过 */
+            if(m_isSeek)
+            {
+                if( std::abs(m_currentPos - tmpPos) > AV_TIME_BASE)
+                {
+                    SPDLOG_DEBUG("currentPos:{}, tmpPos:{}",m_currentPos,tmpPos);
+                    av_packet_unref(m_packet);
+                    continue;
+                }
+            }
+            m_isSeek = false;
+            m_currentPos = tmpPos;
+            // SPDLOG_DEBUG("======== 当前位置:{} ========",m_currentPos);
+            // std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now();
+            /* 解码视频帧,现在使用新的API */
+            ret  = avcodec_send_packet(m_pCodecCtx, m_packet);
+            // std::chrono::steady_clock::time_point t4 = std::chrono::steady_clock::now();
+            if(ret < 0)
+            {
+                SPDLOG_ERROR("发送数据包错误...");
+                continue;
+            }
+            // SPDLOG_TRACE("======================================= 发送数据包耗时:{}",std::chrono::duration_cast<std::chrono::milliseconds>(t4 - t3).count());
+            while (m_isRunning)
+            {
+                /* 从解码器接收解码后的帧的函数。*/
+                retFrame = avcodec_receive_frame(m_pCodecCtx, m_pFrame);
+                std::chrono::steady_clock::time_point t5 = std::chrono::steady_clock::now();
+                // SPDLOG_TRACE("======================================= 接收帧耗时:{}",std::chrono::duration_cast<std::chrono::milliseconds>(t5 - t4).count());
+                
+                if (retFrame == 0)
+                {
+                    /* 成功接收到一帧,处理解码后的帧 */
+                    // std::chrono::steady_clock::time_point t6 = std::chrono::steady_clock::now();
+                    /* 将帧转换为RGB */
+                    sws_scale(m_sws_ctx, (uint8_t const * const *)m_pFrame->data, m_pFrame->linesize, 0, m_pCodecCtx->height, 
+                                        m_pFrameRGB->data, m_pFrameRGB->linesize);
+                    // std::chrono::steady_clock::time_point t7 = std::chrono::steady_clock::now();
+                    // SPDLOG_TRACE("======================================= 转换RGB耗时:{}",std::chrono::duration_cast<std::chrono::milliseconds>(t7 - t6).count());
+                    /* 一帧图像入队 */
+                    auto image = new QImage(m_pFrameRGB->data[0], m_width, m_height, QImage::Format_RGB888);
+                    // SPDLOG_DEBUG("队列中图片个数:{} ",m_queueImage.count());
+                    m_mutexQueue.lock();
+                    if(m_queueImage.count() >= m_queueMaxNum)
+                    {
+                        // SPDLOG_TRACE("队列满了...");
+                        m_condQueueNoFull.wait(&m_mutexQueue);
+                    }
+                    m_queueImage.enqueue(image);
+                    m_mutexQueue.unlock();
+                    /* 队列有数据,唤醒阻塞函数 */
+                    m_condQueueNoEmpty.wakeAll();
+                    /* 同时发送信号 */
+                    emit signal_oneImage();
+
+                    retFrame = -1;
+                }
+                else if(retFrame < 0)
+                {
+                    if( retFrame == AVERROR(EAGAIN) )
+                    {
+                        // SPDLOG_TRACE("在此状态下输出不可用-用户必须尝试发送新的输入...");
+                    }
+                    else if(retFrame == AVERROR_EOF)
+                    {
+                        // SPDLOG_TRACE("读取到文件末尾...");
+                    }else
+                    {
+                        // SPDLOG_TRACE("其他解码错误...{}",retFrame);
+                    }
+                    av_frame_unref(m_pFrame);
+                    break;
+                }
+                else
+                {
+                    SPDLOG_TRACE("其他错误...{}",retFrame);
+                }
+                av_frame_unref(m_pFrame);
+            }
+            
+        }else
+        {
+            // SPDLOG_TRACE("不是视频帧...");
+        }
+        // av_free_packet(AVPacket *pkt)
+        av_packet_unref(m_packet);
+    }
+    
+    freeFFmpeg();
+    /* 清空队列 */
+    m_mutexQueue.lock();
+    int count = m_queueImage.count();
+    for (int i = 0; i < count; i++)
+    {
+        auto image = m_queueImage.dequeue();
+        if (image)
+        {
+            delete image;
+            image = nullptr;
+        }
+    }
+    m_mutexQueue.unlock();
+    /* 线程已停止 */
+    m_threadStopped = true;
+}
+
+/* 退出线程,将所有可能暂停线程运行的条件全部唤醒 */
+void DecodeVedio::exitThread()
+{
+    if(m_isRunning)
+    {
+        m_isRunning = false;
+    }
+    m_pauseDecode = false;
+    m_condQueueNoFull.wakeAll();
+}
+
+/* 暂停解码,会阻塞到线程暂停为止 */
+void DecodeVedio::pauseDecode()
+{
+    m_pauseDecode = true;
+    while (m_decodeStatus) {
+        /* 需要唤醒阻塞住的线程 */
+        m_condQueueNoFull.wakeAll();
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+
+/* 开启解码 */
+void DecodeVedio::do_startDecodeVedio()
+{
+    SPDLOG_DEBUG("线程ID:{}",QThread::currentThreadId());
+    // if(!m_initFFmpeg)
+    // {
+    //     initFFmpeg();
+    // }
+    m_isRunning = true;
+    m_threadStopped = false;
+    m_pauseDecode = false;
+    /* 进入解码,直到播放完成或者手动退出 */
+    decodeVedio();
+    SPDLOG_TRACE("Decode播放完成...");
+
+}

+ 133 - 0
demo/VideoPlayer/VideoPlayer/DecodeVedio.h

@@ -0,0 +1,133 @@
+#ifndef DECODEVEDIO_H
+#define DECODEVEDIO_H
+
+#include <QObject>
+#include <QQueue>
+#include <QTimer>
+#include <QMutex>
+#include <QWaitCondition>
+
+// #include "threadcontroller.h"
+
+extern "C"
+{
+// #include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+// #include <libswscale/swscale.h>
+// #include <libavutil/imgutils.h>
+}
+
+/**
+ * 使用方式:
+ *      1. 初始化FFmpeg:initFFmpeg()
+ *      2. 开启解码线程:startDecodeVedio()
+ *      3. 获取一帧图像:getOneImage()
+ *      4. 停止解码线程:stopDecodeVedio()
+ *      5. 在初始化完成后,未进入第二步的之前,可以获取视频的宽高信息,也可以设置宽高信息
+ *      
+ */
+class DecodeVedio : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit DecodeVedio(QThread* thread, QObject* parent = nullptr);
+    ~DecodeVedio();
+
+    /* 获取硬件解码器 */
+    void getHWDecoder();
+    // void setFileName(const QString& fileName) { m_fileName = fileName; }
+    /* 初始化函数 */
+    void initFFmpeg(const QString& fileName);
+    /* 取消ffmpeg初始化函数 */
+    void unInitFFmpeg();
+    /* 获取ffmpeg初始化状态 */
+    bool isInitFFmpeg() { return m_initFFmpeg; }     
+    
+    /* 开始解码视频,开始前先设置视频名称 */
+    void startDecodeVedio();
+    /* 停止解码视频,也是停止线程 */
+    void stopDecodeVedio();
+    /* 设置当前播放位置 */
+    void setCurrentPos(quint64 pos);
+    /* 获取当前播放位置 */
+    qint64 getCurrentPos();
+    /* 获取视频时长 */
+    qint64 getDuration();
+    /* 获取解码线程是否运行 */
+    bool isRunning() { return m_isRunning; }        
+
+    /* 唤醒队列非空条件变量 */
+    void wakeUpCondQueueNoEmpty();
+    /* 获取一帧图像 */                  
+    QImage* getOneImage();
+    /* 获取一帧图像,直到有图像为止 */                          
+    QImage* getOneImageUntilHave();                 
+
+    /* 获取帧数 */
+    int getFrameCount();                            
+    /* 获取图像宽度 */
+    int getSrcVideoWidth() const {return m_srcWidth;}
+    /* 获取图像高度 */
+    int getSrcVideoHeight() const {return m_srcHeight;}
+    /* 设置图像宽度和高度 */
+    void setVideoSize(int width,int height);
+
+signals:
+    void signal_oneImage();                         /* 一帧图像信号 */
+    void signal_playCompleted();                    /* 播放完成信号 */
+private:
+    void freeFFmpeg();                              /* 取消ffmpeg初始化函数 */
+    void decodeVedio();                             /* 解码视频的工作函数,这个就是新线程 */
+    void exitThread();                              /* 退出线程 */
+    void pauseDecode();                             /* 暂停解码 */
+
+private slots:
+    void do_startDecodeVedio();                     /* 开启解码 */
+
+private:
+    QThread* m_thread = nullptr;                    /* 解码线程 */
+    std::atomic_bool m_isRunning = false;           /* 解码线程是否运行 */
+    QTimer m_startThread;                           /* 开启线程的定时器 */
+    std::atomic_bool m_initFFmpeg = false;          /* ffmpeg初始化标志 */
+    std::atomic_bool m_threadStopped = false;       /* 停止线程 */
+    std::atomic_bool m_pauseDecode = false;         /* 暂停解码 */
+    std::atomic_bool m_decodeStatus = false;        /* 解码状态,这里主要是检测是否暂停解码 */
+    std::atomic_bool m_isSeek = false;              /* 是否跳转 */
+
+    QString m_fileName;                             /* 解码的视频文件名称 */
+    AVFormatContext *m_pFormatContext = nullptr;    /* 格式上下文,贯穿全局 */
+    AVCodecContext *m_pCodecCtx = nullptr;          /* 解码器上下文 */
+    AVPacket* m_packet = nullptr;                   /* 存储解码前的数据,一个数据包 */
+    AVFrame* m_pFrame = nullptr;                    /* 存储解码后的一帧数据原始视频编码 */
+    AVFrame* m_pFrameRGB = nullptr;                 /* 存储解码后的一帧数据,RGB格式 */
+    struct SwsContext *m_sws_ctx = nullptr;         /* 视频转换上下文 */
+    uint8_t *m_buffer = nullptr;                    /* 存储解码后的一帧数据,RGB格式 */
+    int m_videoStream = -1;                         /* 记录视频流是第几个流 */
+
+    int m_srcWidth = 0;                             /* 图片原本宽度 */
+    int m_srcHeight = 0;                            /* 图片原本高度 */
+    int m_width = 0;                                /* 图像宽度 */
+    int m_height = 0;                               /* 图像高度 */
+    int m_frameCount = 0;                           /* 帧数 */
+    int m_fps;                                      /* 每秒的帧数 */
+    qint64 m_duration = 0;                          /* 视频时长 */
+    qint64 m_currentPos = 0;                        /* 当前播放位置 */
+    qint64 m_startPos = 0;                          /* 起始位置,后续操作都需要在这各基础上操作 */
+
+    // QImage *m_image = nullptr;
+    int m_queueMaxNum = 10;                         /* 这里不是环形队列,最大设置为30帧图像 */
+    QQueue<QImage*> m_queueImage;                   /* 视频帧队列 */
+    QMutex m_mutexQueue;                            /* 队列互斥锁 */
+    QWaitCondition m_condQueueNoFull;               /* 队列不满,可以入队 */
+    QWaitCondition m_condQueueNoEmpty;              /* 队列非空,可以读取 */
+
+    QList<int> m_listHWDeviceType;                  /* 保存当前环境支持的硬件解码器 */
+    AVBufferRef* m_hw_device_ctx = nullptr;         /* 对数据缓冲区的引用 */
+};
+
+
+
+
+
+#endif /* DECODEVEDIO_H */

+ 387 - 0
demo/VideoPlayer/VideoPlayer/VideoPlayer.cpp

@@ -0,0 +1,387 @@
+#include "VideoPlayer.h"
+
+#include "DecodeVedio.h"
+
+#include <QPainter>
+#include <QResizeEvent>
+#include <QEventLoop>
+
+#include "spdlog/spdlog.h"
+
+
+
+VideoPlayer::VideoPlayer(QWidget *parent) : QWidget(parent)
+{
+    /* 初始化解码线程 */
+    m_threadDecode = new QThread(this);
+    m_decodeVedio = new DecodeVedio(m_threadDecode);
+
+    m_semRefresh = new QSemaphore(0);
+
+    m_timerRefreshUI.setSingleShot(false);
+    /* 设置精度毫秒级 */
+    m_timerRefreshUI.setTimerType(Qt::PreciseTimer);
+    connect(&m_timerRefreshUI, &QTimer::timeout, this, &VideoPlayer::do_refreshUI);
+
+    connect(m_decodeVedio, &DecodeVedio::signal_oneImage, this, &VideoPlayer::do_refreshOneUI);
+    connect(m_decodeVedio, &DecodeVedio::signal_playCompleted, this, &VideoPlayer::do_playCompleted);
+    SPDLOG_TRACE("UI线程ID:{}", QThread::currentThreadId());
+}
+
+VideoPlayer::~VideoPlayer()
+{
+    if(m_timerRefreshUI.isActive())
+    {
+        m_timerRefreshUI.stop();
+    }
+    delete m_decodeVedio;
+    if(m_image)
+    {
+        delete m_image;
+    }
+}
+
+/**
+ * @brief 设置播放视频,启动定时器,定时器间隔决定播放的速度
+ *        视频的宽和高使用QImage进行缩放
+ *        视频大小在直接设置这个类的resize即可,有最小大小限制
+ * 
+ * @param fileName 
+ */
+void VideoPlayer::setPlayVedio(const QString& fileName)
+{
+    if(isSetVedioFile)
+    {
+        isSetVedioFile = false;
+    }
+    if(m_decodeVedio->isInitFFmpeg())
+    {
+        m_decodeVedio->unInitFFmpeg();
+    }
+    m_fileName = fileName;
+    isSetVedioFile = true;
+    m_decodeVedio->initFFmpeg(m_fileName);
+    // m_decodeVedio->setVideoSize(this->width(), this->height());
+    /* 获取原始视频信息 */
+    m_srcWidth = m_decodeVedio->getSrcVideoWidth();
+    m_srcHeight = m_decodeVedio->getSrcVideoHeight();
+    m_frameCount = m_decodeVedio->getFrameCount();
+    SPDLOG_DEBUG("视频宽:{} 高:{} 帧数:{}", m_srcWidth, m_srcHeight, m_frameCount);
+    /* 设置视频宽和高的最小大小 */
+    this->setMinimumSize(160,90);
+    /* 开启定时器刷新 */
+    if(m_frameCount < 0)
+    {
+        /* HEVC帧率,获取不到,就按照24帧来刷新 */
+        if(m_frameCount == -2)
+        {
+            m_frameCount = 24;
+        }
+        else {
+            m_frameCount = 25;
+        }
+    }
+    else if(m_frameCount == 0)
+    {
+        m_frameCount = 25;
+    }
+    SPDLOG_INFO("帧率:{}", m_frameCount);
+
+    /* 开启解码,手动刷新第一帧 */
+    m_decodeVedio->startDecodeVedio();
+    m_semRefresh->release(2);
+    // m_timerRefreshUI.setSingleShot(true);
+    // m_timerRefreshUI.start(500);
+}
+
+/* 播放视频 */
+bool VideoPlayer::play()
+{
+    if(!isSetVedioFile)
+    {
+        SPDLOG_ERROR("文件名为空");
+        return false;
+    }
+    if(m_playStatus)
+    {
+        return false;
+    }
+    
+    /* 设置刷新时间 */
+    m_timerRefreshUI.setSingleShot(false);
+    m_interval = 1000/m_frameCount;
+    if(1000 % m_frameCount > 5)
+    {
+        m_frameCount += 1;
+    }
+    SPDLOG_TRACE("刷新UI的定时间隔:{}",m_interval);
+    m_timerRefreshUI.start(m_interval);
+    m_playStatus = true;
+    
+    return true;
+}
+
+/* 暂停播放 */
+void VideoPlayer::pause()
+{
+    if(!m_playStatus)
+    {
+        return;
+    }
+    m_timerRefreshUI.stop();
+    m_playStatus = false;
+}
+
+/* 停止播放,停止后停止解码,将时间等复位到开始时间 */
+void VideoPlayer::stop()
+{
+    SPDLOG_DEBUG("...停止播放...");
+    // m_fileName = QString();
+    if(m_timerRefreshUI.isActive())
+    {
+        m_timerRefreshUI.stop();
+    }
+    // SPDLOG_DEBUG("...停止解码...");
+    
+    /* 重新设置播放视频 */
+    setPlayVedio(m_fileName);
+
+    /* 绘制黑帧 */
+    // SPDLOG_DEBUG("绘制黑帧");
+    // m_image = new QImage(m_nowWidth, m_nowHeight, QImage::Format_RGB32);
+    // m_image->fill(Qt::black);
+    // update();
+    m_playStatus = false;
+    // isSetVedioFile = false;
+}
+
+/* 获取视频时长 */
+qint64 VideoPlayer::getDuration()
+{
+    return m_decodeVedio->getDuration();
+}
+
+/* 获取当前播放位置 */
+qint64 VideoPlayer::getCurrentPos()
+{
+    return m_decodeVedio->getCurrentPos();
+}
+
+/* 设置当前播放位置 */
+void VideoPlayer::setCurrentPos(quint64 pos)
+{
+    /* 先停止播放 */
+    bool temp = m_playStatus;
+    if(m_playStatus)
+    {
+        m_timerRefreshUI.stop();
+        m_playStatus = false;
+    }
+    m_decodeVedio->setCurrentPos(pos);
+    /* 继续播放 */
+    if(temp)
+    {
+        // SPDLOG_INFO("..........开启定时器..........");
+        m_timerRefreshUI.start(m_interval);
+        m_playStatus = true;
+    }else
+    {
+        /* 刷新5张照片,防止第一张是跳转前的时间段 */
+        m_semRefresh->release(5);
+    }
+}
+
+/* 设置播放视频大小 */
+void VideoPlayer::setPlayVedioSize(int width,int height)
+{
+    /* 对宽和高就行缩放,保持比例,同时将其居中放置
+     * 先计算出比例,和16/9相对比
+     * 大于16/9,以高为最大极限,计算出宽度和x坐标
+     * 小于16/9,以宽为最大极限,计算出高度和y坐标 */
+    double srcRatio = m_srcWidth*1.0 / m_srcHeight;
+    double ratio = width*1.0 / height;
+    long w1 = 0, h1 = 0;
+    int srcX = this->pos().rx(), srcY = this->pos().ry();
+    int x1 = srcX, y1 = srcY;
+    if(ratio > srcRatio)
+    {
+        w1 = height * srcRatio;
+        x1 = (width - w1) / 2;
+        h1 = height;
+        y1 = srcY;
+    }
+    else if(ratio < srcRatio)
+    {
+        h1 = width / srcRatio;
+        y1 = (height - h1) / 2;
+        w1 = width;
+        x1 = srcX;
+    }else {
+        w1 = width;
+        h1 = height;
+        x1 = srcX;
+        y1 = srcY;
+    }
+    this->move(x1, y1);
+    
+    m_nowWidth = w1;
+    m_nowHeight = h1;
+    this->resize(w1, h1);
+    // SPDLOG_DEBUG("设置窗口位置:{}x{}, 大小:{}x{}, 传入大小:{}x{}", x1, y1, w1, h1, width, height);
+    SPDLOG_DEBUG("现在位置和大小:{}x{}, {}x{}", this->pos().rx(), this->pos().ry(), this->width(), this->height());
+}
+
+/* 设置播放回调函数 */
+// void VideoPlayer::setPlayCallBack(std::function<Play_CallBack> playCallBack,void* context)
+// {
+//     m_funcPlayCB = playCallBack;
+//     m_context = context;
+// }
+
+
+void VideoPlayer::paintEvent(QPaintEvent *event)
+{
+    if(m_image)
+    {
+        // SPDLOG_TRACE("开始绘制画面...");
+        /* 对图像进行缩放 */
+        QImage image;
+        if(m_srcWidth != m_nowWidth || m_srcHeight != m_nowHeight)
+        {
+            image = m_image->scaled(m_nowWidth, m_nowHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+        }
+        QPainter painter(this);
+        painter.drawImage(0, 0, image);
+    }
+}
+
+void VideoPlayer::resizeEvent(QResizeEvent *event)
+{
+    SPDLOG_TRACE("窗口大小改变...");
+    m_nowWidth = event->size().width();
+    m_nowHeight = event->size().height();
+
+    /* 传递给解码器 */
+    // m_decodeVedio->setVideoSize(m_nowWidth, m_nowHeight);
+
+    QWidget::resizeEvent(event);
+}
+
+/* 刷新一张图片,直到有图片为止 */
+void VideoPlayer::refreshOneUIUntilHave()
+{
+    if(m_decodeVedio != nullptr)
+    {
+        // SPDLOG_DEBUG("取出一帧图片...");
+        /* 删除上一帧图片 */
+        if(m_image != nullptr)
+        {
+            delete m_image;
+            m_image = nullptr;
+        }
+        /* 如果没有图片,这个函数会阻塞 */
+        m_image = m_decodeVedio->getOneImageUntilHave();
+        
+        if(m_image)
+        {
+            // if(m_srcWidth != m_nowWidth || m_srcHeight != m_nowHeight)
+            // {
+            //     *m_image = m_image->scaled(m_nowWidth, m_nowHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+            // }
+            // SPDLOG_DEBUG("绘制画面...");
+            update();
+        }
+        m_decodeVedio->wakeUpCondQueueNoEmpty();
+    }
+}
+
+
+/* 双击事件函数 */
+void VideoPlayer::mouseDoubleClickEvent(QMouseEvent *event)
+{
+    if(event->button() == Qt::LeftButton)
+    {
+        // SPDLOG_DEBUG("双击事件...");
+        // if(m_funcPlayCB != nullptr)
+        // {
+        //     m_funcPlayCB(this, 5, nullptr, 0, m_context);
+        // }else {
+        //     SPDLOG_INFO("没有设置回调函数");
+        // }
+    }
+}
+
+/* 取出画面,刷新UI */
+void VideoPlayer::do_refreshUI()
+{
+    if(m_decodeVedio != nullptr)
+    {
+        // SPDLOG_DEBUG("取出一帧图片...");
+        /* 删除上一帧图片 */
+        if(m_image != nullptr)
+        {
+            delete m_image;
+            m_image = nullptr;
+        }
+        m_image = m_decodeVedio->getOneImage();
+        
+        if(m_image)
+        {
+            if(m_srcWidth != m_nowWidth || m_srcHeight != m_nowHeight)
+            {
+                *m_image = m_image->scaled(m_nowWidth, m_nowHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+            }
+            // SPDLOG_DEBUG("绘制画面...");
+            update();
+        }
+        m_decodeVedio->wakeUpCondQueueNoEmpty();
+    }
+}
+
+
+/* 通过信号刷新第一张图片 */
+void VideoPlayer::do_refreshOneUI()
+{
+    if(!m_semRefresh->tryAcquire(1))
+    {
+        return;
+    }
+    /* 取出第一张 */
+    if(m_decodeVedio != nullptr)
+    {
+        // SPDLOG_DEBUG("取出一帧图片...");
+        /* 删除上一帧图片 */
+        if(m_image != nullptr)
+        {
+            delete m_image;
+            m_image = nullptr;
+        }
+        m_image = m_decodeVedio->getOneImage();
+        
+        if(m_image)
+        {
+            if(m_srcWidth != m_nowWidth || m_srcHeight != m_nowHeight)
+            {
+                *m_image = m_image->scaled(m_nowWidth, m_nowHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+            }
+            SPDLOG_DEBUG("绘制预览画面...");
+            update();
+        }
+        m_decodeVedio->wakeUpCondQueueNoEmpty();
+    }
+}
+
+/* 播放完成 */
+void VideoPlayer::do_playCompleted()
+{
+    SPDLOG_INFO("Video 播放完成...");
+    m_timerRefreshUI.stop();
+    m_playStatus = false;
+    // if(m_funcPlayCB != nullptr)
+    // {
+    //     /* 播放完成的回调函数 */
+    //     m_funcPlayCB(this, 2, nullptr, 0, m_context);
+    // }
+}
+
+

+ 64 - 0
demo/VideoPlayer/VideoPlayer/VideoPlayer.h

@@ -0,0 +1,64 @@
+#ifndef VideoPlayer_H
+#define VideoPlayer_H
+
+#include <QWidget>
+#include <QThread>
+#include <QTimer>
+#include <QSemaphore>
+
+class DecodeVedio;
+
+class VideoPlayer : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit VideoPlayer(QWidget *parent = nullptr);
+    ~VideoPlayer();
+
+    void setPlayVedio(const QString& fileName);     /* 设置播放视频 */
+    bool play();                                    /* 播放视频 */
+    void pause();                                   /* 暂停播放 */
+    void stop();                                    /* 停止播放 */
+    bool getPlayStatus() { return m_playStatus; }   /* 获取播放状态 */
+    qint64 getDuration();                           /* 获取视频时长 */
+    qint64 getCurrentPos();                         /* 获取当前播放位置 */
+    void setCurrentPos(quint64 pos);                /* 设置当前播放位置 */
+    void setPlayVedioSize(int width,int height);    /* 设置播放视频大小 */
+
+    // void setPlayCallBack(std::function<Play_CallBack> playCallBack,void* context);  /* 设置播放回调函数 */
+protected:
+    void paintEvent(QPaintEvent *event) override;
+    void resizeEvent(QResizeEvent *event) override;
+    void refreshOneUIUntilHave();                   /* 刷新一张图片,直到有图片为止 */
+    /* 双击事件函数 */
+    void mouseDoubleClickEvent(QMouseEvent *event) override;
+
+private slots:
+    void do_refreshUI();                            /* 取出画面,刷新UI */
+    void do_refreshOneUI();                         /* 通过信号刷新第一张图片 */
+    void do_playCompleted();                        /* 播放完成 */
+
+private:
+    QString m_fileName;
+    QTimer m_timerRefreshUI;                        /* 定时器,用于刷新界面 */
+    int m_srcWidth = 0;                             /* 图片原本大小 */
+    int m_srcHeight = 0;
+    int m_nowWidth = 0;                             /* 现在大小 */
+    int m_nowHeight = 0;
+    int m_frameCount = 0;                           /* 帧数 */
+    int m_interval = 0;                             /* 间隔 */
+
+    DecodeVedio* m_decodeVedio = nullptr;
+    QThread* m_threadDecode = nullptr;              /* 解码器所在的线程 */
+    QImage* m_image = nullptr;                      /* 画面 */
+    bool m_playStatus = false;                      /* 是否正在播放 */
+    bool isSetVedioFile = false;                    /* 是否设置了视频文件 */
+    QSemaphore* m_semRefresh = nullptr;             /* 刷新信号量 */
+
+    // std::function<Play_CallBack> m_funcPlayCB = nullptr;  /* 播放回调函数 */
+    void* m_context = nullptr;                      /* 上下文 */
+};
+
+
+
+#endif /* VideoPlayer_H */

+ 572 - 0
demo/VideoPlayer/demo/videodecode.cpp

@@ -0,0 +1,572 @@
+#include "videodecode.h"
+#include <QDebug>
+#include <QImage>
+#include <QMutex>
+#include <qdatetime.h>
+
+
+extern "C" {        // 用C规则编译指定的代码
+#include "libavcodec/avcodec.h"
+#include "libavformat/avformat.h"
+#include "libavutil/avutil.h"
+#include "libswscale/swscale.h"
+#include "libavutil/imgutils.h"
+
+}
+
+#define ERROR_LEN 1024  // 异常信息数组长度
+#define PRINT_LOG 1
+
+VideoDecode::VideoDecode()
+{
+//    initFFmpeg();      // 5.1.2版本不需要调用了
+
+    m_error = new char[ERROR_LEN];
+
+    /*************************************** 获取当前环境支持的硬件解码器 *********************************************/
+    AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;      // ffmpeg支持的硬件解码器
+    QStringList strTypes;
+    while ((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE)       // 遍历支持的设备类型。
+    {
+        m_HWDeviceTypes.append(type);
+        const char* ctype = av_hwdevice_get_type_name(type);  // 获取AVHWDeviceType的字符串名称。
+        if(ctype)
+        {
+            strTypes.append(QString(ctype));
+        }
+    }
+    qDebug() << "支持的硬件解码器:" << strTypes;
+    /************************************************ END ******************************************************/
+}
+
+VideoDecode::~VideoDecode()
+{
+    close();
+}
+
+/**
+ * @brief 初始化ffmpeg库(整个程序中只需加载一次)
+ *        旧版本的ffmpeg需要注册各种文件格式、解复用器、对网络库进行全局初始化。
+ *        在新版本的ffmpeg中纷纷弃用了,不需要注册了
+ */
+void VideoDecode::initFFmpeg()
+{
+    static bool isFirst = true;
+    static QMutex mutex;
+    QMutexLocker locker(&mutex);
+    if(isFirst)
+    {
+        //        av_register_all();         // 已经从源码中删除
+        /**
+         * 初始化网络库,用于打开网络流媒体,此函数仅用于解决旧GnuTLS或OpenSSL库的线程安全问题。
+         * 一旦删除对旧GnuTLS和OpenSSL库的支持,此函数将被弃用,并且此函数不再有任何用途。
+         */
+        avformat_network_init();
+        isFirst = false;
+    }
+}
+
+/*********************************** FFmpeg获取GPU硬件解码帧格式的回调函数 *****************************************/
+static enum AVPixelFormat g_pixelFormat;
+/**
+ * @brief      回调函数,获取GPU硬件解码帧的格式
+ * @param s
+ * @param fmt
+ * @return
+ */
+AVPixelFormat get_hw_format(AVCodecContext* s, const enum AVPixelFormat* fmt)
+{
+    Q_UNUSED(s)
+    const enum AVPixelFormat* p;
+
+    for (p = fmt; *p != -1; p++)
+    {
+        if(*p == g_pixelFormat)
+        {
+            return *p;
+        }
+    }
+
+    qDebug() << "无法获取硬件表面格式.";         // 当同时打开太多路视频时,如果超过了GPU的能力,可能会返回找不到解码帧格式
+    return AV_PIX_FMT_NONE;
+}
+/************************************************ END ******************************************************/
+
+/**************************************** FFmpeg初始化硬件解码器 **********************************************/
+/**
+ * @brief         初始化硬件解码器
+ * @param codec
+ */
+void VideoDecode::initHWDecoder(const AVCodec *codec)
+{
+    if(!codec) return;
+
+    for(int i = 0; ; i++)
+    {
+        const AVCodecHWConfig* config = avcodec_get_hw_config(codec, i);    // 检索编解码器支持的硬件配置。
+        if(!config)
+        {
+            qDebug() << "打开硬件解码器失败!";
+            return;          // 没有找到支持的硬件配置
+        }
+
+        if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)       // 判断是否是设备类型
+        {
+            for(auto i : m_HWDeviceTypes)
+            {
+                if(config->device_type == AVHWDeviceType(i))                 // 判断设备类型是否是支持的硬件解码器
+                {
+                    g_pixelFormat = config->pix_fmt;
+
+                    // 打开指定类型的设备,并为其创建AVHWDeviceContext。
+                    int ret = av_hwdevice_ctx_create(&hw_device_ctx, config->device_type, nullptr, nullptr, 0);
+                    if(ret < 0)
+                    {
+                        showError(ret);
+                        free();
+                        return ;
+                    }
+                    qDebug() << "打开硬件解码器:" << av_hwdevice_get_type_name(config->device_type);
+                    m_codecContext->hw_device_ctx = av_buffer_ref(hw_device_ctx);  // 创建一个对AVBuffer的新引用。
+                    m_codecContext->get_format = get_hw_format;                    // 由一些解码器调用,以选择将用于输出帧的像素格式
+                    return;
+                }
+            }
+        }
+    }
+}
+
+/************************************************ END ******************************************************/
+
+/**
+ * @brief      打开媒体文件,或者流媒体,例如rtmp、strp、http
+ * @param url  视频地址
+ * @return     true:成功  false:失败
+ */
+bool VideoDecode::open(const QString &url)
+{
+    if(url.isNull()) return false;
+
+    AVDictionary* dict = nullptr;
+    av_dict_set(&dict, "rtsp_transport", "tcp", 0);      // 设置rtsp流使用tcp打开,如果打开失败错误信息为【Error number -135 occurred】可以切换(UDP、tcp、udp_multicast、http),比如vlc推流就需要使用udp打开
+    av_dict_set(&dict, "max_delay", "3", 0);             // 设置最大复用或解复用延迟(以微秒为单位)。当通过【UDP】 接收数据时,解复用器尝试重新排序接收到的数据包(因为它们可能无序到达,或者数据包可能完全丢失)。这可以通过将最大解复用延迟设置为零(通过max_delayAVFormatContext 字段)来禁用。
+    av_dict_set(&dict, "timeout", "1000000", 0);         // 以微秒为单位设置套接字 TCP I/O 超时,如果等待时间过短,也可能会还没连接就返回了。
+
+    // 打开输入流并返回解封装上下文
+    int ret = avformat_open_input(&m_formatContext,          // 返回解封装上下文
+                                  url.toStdString().data(),  // 打开视频地址
+                                  nullptr,                   // 如果非null,此参数强制使用特定的输入格式。自动选择解封装器(文件格式)
+                                  &dict);                    // 参数设置
+    // 释放参数字典
+    if(dict)
+    {
+        av_dict_free(&dict);
+    }
+    // 打开视频失败
+    if(ret < 0)
+    {
+        showError(ret);
+        free();
+        return false;
+    }
+
+    // 读取媒体文件的数据包以获取流信息。
+    ret = avformat_find_stream_info(m_formatContext, nullptr);
+    if(ret < 0)
+    {
+        showError(ret);
+        free();
+        return false;
+    }
+    m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000); // 计算视频总时长(毫秒)
+#if PRINT_LOG
+    qDebug() << QString("视频总时长:%1 ms,[%2]").arg(m_totalTime).arg(QTime::fromMSecsSinceStartOfDay(int(m_totalTime)).toString("HH:mm:ss zzz"));
+#endif
+
+    // 通过AVMediaType枚举查询视频流ID(也可以通过遍历查找),最后一个参数无用
+    m_videoIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
+    if(m_videoIndex < 0)
+    {
+        showError(m_videoIndex);
+        free();
+        return false;
+    }
+
+    AVStream* videoStream = m_formatContext->streams[m_videoIndex];  // 通过查询到的索引获取视频流
+
+    // 获取视频图像分辨率(AVStream中的AVCodecContext在新版本中弃用,改为使用AVCodecParameters)
+    m_size.setWidth(videoStream->codecpar->width);
+    m_size.setHeight(videoStream->codecpar->height);
+    m_frameRate = rationalToDouble(&videoStream->avg_frame_rate);  // 视频帧率
+
+    // 通过解码器ID获取视频解码器(新版本返回值必须使用const)
+    const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
+    m_totalFrames = videoStream->nb_frames;
+
+#if PRINT_LOG
+    qDebug() << QString("分辨率:[w:%1,h:%2] 帧率:%3  总帧数:%4  解码器:%5")
+                .arg(m_size.width()).arg(m_size.height()).arg(m_frameRate).arg(m_totalFrames).arg(codec->name);
+#endif
+
+    // 分配AVCodecContext并将其字段设置为默认值。
+    m_codecContext = avcodec_alloc_context3(codec);
+    if(!m_codecContext)
+    {
+#if PRINT_LOG
+        qWarning() << "创建视频解码器上下文失败!";
+#endif
+        free();
+        return false;
+    }
+
+    // 使用视频流的codecpar为解码器上下文赋值
+    ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);
+    if(ret < 0)
+    {
+        showError(ret);
+        free();
+        return false;
+    }
+
+    m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;    // 允许不符合规范的加速技巧。
+    m_codecContext->thread_count = 8;                 // 使用8线程解码
+
+    if(m_HWDecoder)
+    {
+        initHWDecoder(codec);     // 初始化硬件解码器(在avcodec_open2前调用)
+    }
+
+    // 初始化解码器上下文,如果之前avcodec_alloc_context3传入了解码器,这里设置NULL就可以
+    ret = avcodec_open2(m_codecContext, nullptr, nullptr);
+    if(ret < 0)
+    {
+        showError(ret);
+        free();
+        return false;
+    }
+
+    return initObject();
+}
+
+/**
+ * @brief   初始化需要用到的对象
+ * @return
+ */
+bool VideoDecode::initObject()
+{
+    // 分配AVPacket并将其字段设置为默认值。
+    m_packet = av_packet_alloc();
+    if(!m_packet)
+    {
+#if PRINT_LOG
+        qWarning() << "av_packet_alloc() Error!";
+#endif
+        free();
+        return false;
+    }
+    // 分配AVFrame并将其字段设置为默认值。
+    m_frame = av_frame_alloc();
+    if(!m_frame)
+    {
+#if PRINT_LOG
+        qWarning() << "av_frame_alloc() Error!";
+#endif
+        free();
+        return false;
+    }
+    m_frameHW = av_frame_alloc();
+    if(!m_frameHW)
+    {
+#if PRINT_LOG
+        qWarning() << "av_frame_alloc() Error!";
+#endif
+        free();
+        return false;
+    }
+
+
+    // 由于传递时是浅拷贝,可能显示类还没处理完成,所以如果播放完成就释放可能会崩溃;
+    if(m_buffer)
+    {
+        delete [] m_buffer;
+        m_buffer = nullptr;
+    }
+    // 分配图像空间
+    int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_size.width(), m_size.height(), 4);
+    /**
+     * 【注意:】这里可以多分配一些,否则如果只是安装size分配,大部分视频图像数据拷贝没有问题,
+     *         但是少部分视频图像在使用sws_scale()拷贝时会超出数组长度,在使用使用msvc debug模式时delete[] m_buffer会报错(HEAP CORRUPTION DETECTED: after Normal block(#32215) at 0x000001AC442830370.CRT delected that the application wrote to memory after end of heap buffer)
+     *         特别是这个视频流http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4
+     */
+    m_buffer = new uchar[size + 1000];    // 这里多分配1000个字节就基本不会出现拷贝超出的情况了,反正不缺这点内存
+//    m_image = new QImage(m_buffer, m_size.width(), m_size.height(), QImage::Format_RGBA8888);  // 这种方式分配内存大部分情况下也可以,但是因为存在拷贝超出数组的情况,delete时也会报错
+    m_end = false;
+
+    return true;
+}
+
+
+/**
+ * @brief    读取并返回视频图像
+ * @return
+ */
+QImage VideoDecode::read()
+{
+    // 如果没有打开则返回
+    if(!m_formatContext)
+    {
+        return QImage();
+    }
+
+    // 读取下一帧数据
+    int readRet = av_read_frame(m_formatContext, m_packet);
+    if(readRet < 0)
+    {
+        avcodec_send_packet(m_codecContext, m_packet); // 读取完成后向解码器中传如空AVPacket,否则无法读取出最后几帧
+    }
+    else
+    {
+        if(m_packet->stream_index == m_videoIndex)     // 如果是图像数据则进行解码
+        {
+            // 计算当前帧时间(毫秒)
+#if 1       // 方法一:适用于所有场景,但是存在一定误差
+            m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
+            m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
+#else       // 方法二:适用于播放本地视频文件,计算每一帧时间较准,但是由于网络视频流无法获取总帧数,所以无法适用
+            m_obtainFrames++;
+            m_packet->pts = qRound64(m_obtainFrames * (qreal(m_totalTime) / m_totalFrames));
+#endif
+            // 将读取到的原始数据包传入解码器
+            int ret = avcodec_send_packet(m_codecContext, m_packet);
+            if(ret < 0)
+            {
+                showError(ret);
+            }
+        }
+    }
+    av_packet_unref(m_packet);  // 释放数据包,引用计数-1,为0时释放空间
+
+    int ret = avcodec_receive_frame(m_codecContext, m_frame);
+    if(ret < 0)
+    {
+        av_frame_unref(m_frame);
+        if(readRet < 0)
+        {
+            m_end = true;     // 当无法读取到AVPacket并且解码器中也没有数据时表示读取完成
+        }
+        return QImage();
+    }
+
+    // 这样写是为了兼容软解码或者硬件解码打开失败情况
+    AVFrame*  m_frameTemp = m_frame;
+    if(!m_frame->data[0])               // 如果是硬件解码就进入
+    {
+        m_frameTemp = m_frameHW;
+        // 将解码后的数据从GPU拷贝到CPU
+        if(!dataCopy())
+        {
+            return QImage();
+        }
+    }
+
+    m_pts = m_frameTemp->pts;
+
+    // 为什么图像转换上下文要放在这里初始化呢,是因为m_frame->format,如果使用硬件解码,解码出来的图像格式和m_codecContext->pix_fmt的图像格式不一样,就会导致无法转换为QImage
+    if(!m_swsContext)
+    {
+        // 获取缓存的图像转换上下文。首先校验参数是否一致,如果校验不通过就释放资源;然后判断上下文是否存在,如果存在直接复用,如不存在进行分配、初始化操作
+        m_swsContext = sws_getCachedContext(m_swsContext,
+                                            m_frameTemp->width,                   // 输入图像的宽度
+                                            m_frameTemp->height,                  // 输入图像的高度
+                                            (AVPixelFormat)m_frameTemp->format,   // 输入图像的像素格式
+                                            m_size.width(),                     // 输出图像的宽度
+                                            m_size.height(),                    // 输出图像的高度
+                                            AV_PIX_FMT_RGBA,                    // 输出图像的像素格式
+                                            SWS_BILINEAR,                       // 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR
+                                            nullptr,                            // 输入图像的滤波器信息, 若不需要传NULL
+                                            nullptr,                            // 输出图像的滤波器信息, 若不需要传NULL
+                                            nullptr);                           // 特定缩放算法需要的参数(?),默认为NULL
+        if(!m_swsContext)
+        {
+#if PRINT_LOG
+            qWarning() << "sws_getCachedContext() Error!";
+#endif
+            free();
+            return QImage();
+        }
+    }
+
+
+    // AVFrame转QImage
+    uchar* data[]  = {m_buffer};
+    int    lines[4];
+    av_image_fill_linesizes(lines, AV_PIX_FMT_RGBA, m_frame->width);  // 使用像素格式pix_fmt和宽度填充图像的平面线条大小。
+    ret = sws_scale(m_swsContext,             // 缩放上下文
+                    m_frameTemp->data,            // 原图像数组
+                    m_frameTemp->linesize,        // 包含源图像每个平面步幅的数组
+                    0,                        // 开始位置
+                    m_frameTemp->height,          // 行数
+                    data,                     // 目标图像数组
+                    lines);                   // 包含目标图像每个平面的步幅的数组
+    QImage image(m_buffer, m_frameTemp->width, m_frameTemp->height, QImage::Format_RGBA8888);
+    av_frame_unref(m_frame);
+    av_frame_unref(m_frameHW);
+
+    return image;
+}
+
+/********************************* FFmpeg初始化硬件后将图像数据从GPU拷贝到CPU *************************************/
+/**
+ * @brief   硬件解码完成需要将数据从GPU复制到CPU
+ * @return
+ */
+bool VideoDecode::dataCopy()
+{
+    if(m_frame->format != g_pixelFormat)
+    {
+        av_frame_unref(m_frame);
+        return false;
+    }
+    int ret = av_hwframe_transfer_data(m_frameHW, m_frame, 0);       // 将解码后的数据从GPU复制到CPU(m_frameHW) 这一步比较耗时,在这一步之前硬解码速度比软解码快很多
+    if(ret < 0)
+    {
+        showError(ret);
+        av_frame_unref(m_frame);
+        return false;
+    }
+    av_frame_copy_props(m_frameHW, m_frame);   // 仅将“metadata”字段从src复制到dst。
+    return true;
+}
+
+/************************************************ END ******************************************************/
+
+/**
+ * @brief 关闭视频播放并释放内存
+ */
+void VideoDecode::close()
+{
+    clear();
+    free();
+
+    m_totalTime     = 0;
+    m_videoIndex    = 0;
+    m_totalFrames   = 0;
+    m_obtainFrames  = 0;
+    m_pts           = 0;
+    m_frameRate     = 0;
+    m_size          = QSize(0, 0);
+}
+
+/**
+ * @brief  视频是否读取完成
+ * @return
+ */
+bool VideoDecode::isEnd()
+{
+    return m_end;
+}
+
+/**
+ * @brief    返回当前帧图像播放时间
+ * @return
+ */
+const qint64 &VideoDecode::pts()
+{
+    return m_pts;
+}
+
+/**
+ * @brief         设置是否使用硬件解码
+ * @param flag    true:使用 false:不使用
+ */
+void VideoDecode::setHWDecoder(bool flag)
+{
+    m_HWDecoder = flag;
+}
+
+/**
+ * @brief   返回当前是否使用硬件解码
+ * @return
+ */
+bool VideoDecode::isHWDecoder()
+{
+    return m_HWDecoder;
+}
+
+/**
+ * @brief        显示ffmpeg函数调用异常信息
+ * @param err
+ */
+void VideoDecode::showError(int err)
+{
+#if PRINT_LOG
+    memset(m_error, 0, ERROR_LEN);        // 将数组置零
+    av_strerror(err, m_error, ERROR_LEN);
+    qWarning() << "DecodeVideo Error:" << m_error;
+#else
+    Q_UNUSED(err)
+#endif
+}
+
+/**
+ * @brief          将AVRational转换为double,用于计算帧率
+ * @param rational
+ * @return
+ */
+qreal VideoDecode::rationalToDouble(AVRational* rational)
+{
+    qreal frameRate = (rational->den == 0) ? 0 : (qreal(rational->num) / rational->den);
+    return frameRate;
+}
+
+/**
+ * @brief 清空读取缓冲
+ */
+void VideoDecode::clear()
+{
+    // 因为avformat_flush不会刷新AVIOContext (s->pb)。如果有必要,在调用此函数之前调用avio_flush(s->pb)。
+    if(m_formatContext && m_formatContext->pb)
+    {
+        avio_flush(m_formatContext->pb);
+    }
+    if(m_formatContext)
+    {
+        avformat_flush(m_formatContext);   // 清理读取缓冲
+    }
+}
+
+void VideoDecode::free()
+{
+    // 释放上下文swsContext。
+    if(m_swsContext)
+    {
+        sws_freeContext(m_swsContext);
+        m_swsContext = nullptr;             // sws_freeContext不会把上下文置NULL
+    }
+    // 释放编解码器上下文和与之相关的所有内容,并将NULL写入提供的指针
+    if(m_codecContext)
+    {
+        avcodec_free_context(&m_codecContext);
+    }
+    // 关闭并失败m_formatContext,并将指针置为null
+    if(m_formatContext)
+    {
+        avformat_close_input(&m_formatContext);
+    }
+    if(hw_device_ctx)
+    {
+        av_buffer_unref(&hw_device_ctx);
+    }
+    if(m_packet)
+    {
+        av_packet_free(&m_packet);
+    }
+    if(m_frame)
+    {
+        av_frame_free(&m_frame);
+    }
+    if(m_frameHW)
+    {
+        av_frame_free(&m_frameHW);
+    }
+}

+ 74 - 0
demo/VideoPlayer/demo/videodecode.h

@@ -0,0 +1,74 @@
+/******************************************************************************
+ * @文件名     videodecode.h
+ * @功能       视频解码类,在这个类中调用ffmpeg打开视频进行解码
+ *
+ * @开发者     mhf
+ * @邮箱       1603291350@qq.com
+ * @时间       2022/09/15
+ * @备注
+ *****************************************************************************/
+#ifndef VIDEODECODE_H
+#define VIDEODECODE_H
+
+#include <QString>
+#include <QSize>
+#include <qlist.h>
+
+struct AVFormatContext;
+struct AVCodecContext;
+struct AVRational;
+struct AVPacket;
+struct AVFrame;
+struct AVCodec;
+struct SwsContext;
+struct AVBufferRef;
+class QImage;
+
+class VideoDecode
+{
+public:
+    VideoDecode();
+    ~VideoDecode();
+
+    bool open(const QString& url = QString());    // 打开媒体文件,或者流媒体rtmp、strp、http
+    QImage read();                                // 读取视频图像
+    void close();                                 // 关闭
+    bool isEnd();                                 // 是否读取完成
+    const qint64& pts();                          // 获取当前帧显示时间
+    void setHWDecoder(bool flag);                 // 是否使用硬件解码器
+    bool isHWDecoder();
+
+private:
+    void initFFmpeg();                            // 初始化ffmpeg库(整个程序中只需加载一次)
+    void initHWDecoder(const AVCodec* codec);     // 初始化硬件解码器
+    bool initObject();                            // 初始化对象
+    bool dataCopy();                              // 硬件解码完成需要将数据从GPU复制到CPU
+    void showError(int err);                      // 显示ffmpeg执行错误时的错误信息
+    qreal rationalToDouble(AVRational* rational); // 将AVRational转换为double
+    void clear();                                 // 清空读取缓冲
+    void free();                                  // 释放
+
+private:
+    AVFormatContext* m_formatContext = nullptr;   // 解封装上下文
+    AVCodecContext*  m_codecContext  = nullptr;   // 解码器上下文
+    SwsContext*      m_swsContext    = nullptr;   // 图像转换上下文
+    AVPacket* m_packet = nullptr;                 // 数据包
+    AVFrame*  m_frame  = nullptr;                 // 解码后的视频帧
+    AVFrame*  m_frameHW = nullptr;                // 硬件解码后的视频帧
+    int    m_videoIndex   = 0;                    // 视频流索引
+    qint64 m_totalTime    = 0;                    // 视频总时长
+    qint64 m_totalFrames  = 0;                    // 视频总帧数
+    qint64 m_obtainFrames = 0;                    // 视频当前获取到的帧数
+    qint64 m_pts          = 0;                    // 图像帧的显示时间
+    qreal  m_frameRate    = 0;                    // 视频帧率
+    QSize  m_size;                                // 视频分辨率大小
+    char*  m_error = nullptr;                     // 保存异常信息
+    bool   m_end = false;                         // 视频读取完成
+    uchar* m_buffer = nullptr;
+
+    QList<int> m_HWDeviceTypes;                   // 保存当前环境支持的硬件解码器
+    AVBufferRef* hw_device_ctx = nullptr;         // 对数据缓冲区的引用
+    bool   m_HWDecoder = false;                   // 记录是否使用硬件解码
+};
+
+#endif // VIDEODECODE_H

+ 21 - 0
demo/VideoPlayer/main.cpp

@@ -0,0 +1,21 @@
+#include "widget.h"
+
+#include <QApplication>
+#include "Logs/loginit.h"
+#include "spdlog/spdlog.h"
+
+
+
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    init_log();
+
+    SPDLOG_INFO("********** VideoPlayer **********");
+
+    Widget w;
+    w.show();
+
+    return a.exec();
+}

+ 37 - 0
demo/VideoPlayer/widget.cpp

@@ -0,0 +1,37 @@
+#include "widget.h"
+#include "./ui_widget.h"
+
+#include <QTimer>
+
+#include "spdlog/spdlog.h"
+#include "fmtlog.h"
+#include "VideoPlayer.h"
+
+
+Widget::Widget(QWidget *parent)
+    : QWidget(parent)
+    , ui(new Ui::Widget)
+{
+    ui->setupUi(this);
+
+
+    SPDLOG_INFO("***** Qt Library *****");
+}
+
+Widget::~Widget()
+{
+
+    delete ui;
+}
+
+
+void Widget::on_pBtn_openVideo_clicked()
+{
+    SPDLOG_INFO("点击了“打开视频”按钮");
+    
+    std::shared_ptr<VideoPlayer> videoPlayer = std::make_shared<VideoPlayer>();
+    
+}
+
+
+

+ 27 - 0
demo/VideoPlayer/widget.h

@@ -0,0 +1,27 @@
+#ifndef WIDGET_H
+#define WIDGET_H
+
+#include <QWidget>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class Widget; }
+QT_END_NAMESPACE
+
+class Widget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Widget(QWidget *parent = nullptr);
+    ~Widget();
+
+private slots:
+
+    void on_pBtn_openVideo_clicked();
+
+
+private:
+    Ui::Widget *ui;
+
+};
+#endif // WIDGET_H

+ 41 - 0
demo/VideoPlayer/widget.ui

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Widget</string>
+  </property>
+  <widget class="QWidget" name="widget_pBtn" native="true">
+   <property name="geometry">
+    <rect>
+     <x>30</x>
+     <y>30</y>
+     <width>681</width>
+     <height>311</height>
+    </rect>
+   </property>
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QPushButton" name="pBtn_openVideo">
+      <property name="text">
+       <string>打开视频</string>
+      </property>
+     </widget>
+    </item>
+    <item row="1" column="0" colspan="2">
+     <widget class="QLineEdit" name="lineEdit"/>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 0 - 0
demo/VideoPlayer/解码路线图.xmind


+ 79 - 0
demo/time/CMakeLists.txt

@@ -0,0 +1,79 @@
+cmake_minimum_required(VERSION 3.5)
+
+set(this_exe time)
+
+
+#包含源文件
+file(GLOB LOCAL_SRC
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.rc
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
+
+    ${CMAKE_SOURCE_DIR}/External/common/LightLog/*.cpp
+    ${CMAKE_SOURCE_DIR}/External/common/Logs/*.cpp
+    ${CMAKE_SOURCE_DIR}/External/common/ThreadPool/*.cpp
+    
+)
+
+
+# 生成可执行程序
+
+add_executable(${this_exe}
+    # WIN32
+    ${GLOBAL_SRC}
+    ${LOCAL_SRC} 
+)
+
+
+#添加头文件
+target_include_directories(${this_exe} PRIVATE
+
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_SOURCE_DIR}/External/common/LightLog
+    ${CMAKE_SOURCE_DIR}/External/common
+    ${CMAKE_SOURCE_DIR}/External/common/FmtLog
+    ${CMAKE_SOURCE_DIR}/External/common/Timer
+    ${CMAKE_SOURCE_DIR}/External/common/ThreadPool
+    ${CMAKE_SOURCE_DIR}/External/common/RingQueue
+)
+
+target_link_libraries(${this_exe} PRIVATE
+    Qt5::Widgets
+    Qt5::Core
+    Qt5::Network
+    # Qt5::Multimedia
+    # Qt5::Xml
+    # Qt5::Sql
+)
+
+target_link_libraries(${this_exe} PRIVATE 
+    fmt::fmt
+    spdlog::spdlog
+    ${CURL_LIBRARY}
+)
+
+if(CMAKE_CXX_COMPILER_VERSION LESS 9.0)
+    target_link_libraries(${this_exe} PRIVATE
+        stdc++fs
+    )
+endif()
+
+# target_link_libraries(${this_exe} PRIVATE
+#     ${CURL_LIBRARY}
+    
+# )
+# message(STATUS "CURL_LIBRARY: ${CURL_LIBRARY}")
+
+
+# if(CMAKE_CXX_COMPILER_ID MATCHES MSVC)
+#     target_link_libraries(${this_exe} PRIVATE
+#         # debug spdlogd.lib
+#         # optimized spdlog.lib
+#     )
+# elseif(CMAKE_CXX_COMPILER_ID MATCHES GNU)
+#     target_link_libraries(${this_exe} PRIVATE
+#         # debug 
+#         # optimized ${SM_DLL}
+#     )
+# endif()

+ 53 - 0
demo/time/main.cpp

@@ -0,0 +1,53 @@
+#include "widget.h"
+
+#include <QCoreApplication>
+#include "Logs/loginit.h"
+#include "spdlog/spdlog.h"
+#include "Timer/TWTimer.hpp"
+
+
+
+
+void printCPPTime()
+{
+    std::chrono::steady_clock::time_point lastTime = std::chrono::steady_clock::now();
+    long count = 0;
+    while(true)
+    {
+        std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
+        SPDLOG_INFO("C++风格的时间 间隔: {} us", std::chrono::duration_cast<std::chrono::microseconds>(now - lastTime).count());
+        lastTime = now;
+        count++;
+        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+    }
+}
+
+void printCTime()
+{
+    std::time_t lastTime = std::time(nullptr);
+    long count = 0;
+    while(true)
+    {
+        std::time_t now = std::time(nullptr);
+        SPDLOG_INFO("***** C风格的时间 间隔: {} us", std::difftime(now, lastTime));
+        lastTime = now;
+        count++;
+        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    QCoreApplication a(argc, argv);
+    init_log();
+
+    SPDLOG_INFO("********** Time **********");
+
+    // CPPTP.add_task(printCPPTime);
+    // CPPTP.add_task(printCTime);
+    auto now = std::time(nullptr);
+    SPDLOG_INFO("{}", std::ctime(&now));
+
+
+    return a.exec();
+}

+ 54 - 0
demo/time/widget.cpp

@@ -0,0 +1,54 @@
+#include "widget.h"
+#include "./ui_widget.h"
+
+#include <QTimer>
+
+#include "spdlog/spdlog.h"
+#include "fmtlog.h"
+#include "LightLog.h"
+
+
+Widget::Widget(QWidget *parent)
+    : QWidget(parent)
+    , ui(new Ui::Widget)
+{
+    ui->setupUi(this);
+
+
+    SPDLOG_INFO("***** Qt Library *****");
+}
+
+Widget::~Widget()
+{
+
+    delete ui;
+}
+
+
+void Widget::on_pBtn_connect_clicked()
+{
+    SPDLOG_INFO("点击了“连接按钮”");
+    std::vector<std::string> list;
+}
+
+
+
+void Widget::on_pBtn_downloadFile_clicked()
+{
+    SPDLOG_INFO("点击了“下载文件”");
+
+}
+
+void Widget::on_pBtn_downloadVideo_clicked()
+{
+    SPDLOG_INFO("点击了“下载视频”");
+
+   
+}
+
+void Widget::on_pBtn_logSpeed_clicked()
+{
+    SPDLOG_INFO("点击了“日志速度”");
+    
+}
+

+ 32 - 0
demo/time/widget.h

@@ -0,0 +1,32 @@
+#ifndef WIDGET_H
+#define WIDGET_H
+
+#include <QWidget>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class Widget; }
+QT_END_NAMESPACE
+
+class Widget : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Widget(QWidget *parent = nullptr);
+    ~Widget();
+
+private slots:
+
+    void on_pBtn_connect_clicked();
+
+    void on_pBtn_downloadFile_clicked();
+
+    void on_pBtn_downloadVideo_clicked();
+
+    void on_pBtn_logSpeed_clicked();
+
+private:
+    Ui::Widget *ui;
+
+};
+#endif // WIDGET_H

+ 62 - 0
demo/time/widget.ui

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Widget</string>
+  </property>
+  <widget class="QWidget" name="widget_pBtn" native="true">
+   <property name="geometry">
+    <rect>
+     <x>30</x>
+     <y>30</y>
+     <width>681</width>
+     <height>311</height>
+    </rect>
+   </property>
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="1">
+     <widget class="QPushButton" name="pBtn_downloadFile">
+      <property name="text">
+       <string>下载文件</string>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="0">
+     <widget class="QPushButton" name="pBtn_connect">
+      <property name="text">
+       <string>连接</string>
+      </property>
+     </widget>
+    </item>
+    <item row="2" column="0" colspan="2">
+     <widget class="QLineEdit" name="lineEdit"/>
+    </item>
+    <item row="1" column="0">
+     <widget class="QPushButton" name="pBtn_downloadVideo">
+      <property name="text">
+       <string>下载视频</string>
+      </property>
+     </widget>
+    </item>
+    <item row="1" column="1">
+     <widget class="QPushButton" name="pBtn_logSpeed">
+      <property name="text">
+       <string>日志速度对比</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>