Преглед на файлове

V0.4.7
1、本地播放视频基本完成了

Apple преди 7 месеца
родител
ревизия
cb7aaab8cb

+ 1 - 1
External

@@ -1 +1 @@
-/home/Apple/Lib/Standard_Library
+/home/Apple/Libs/Standard_Library

+ 1 - 1
External_Ex

@@ -1 +1 @@
-/home/Apple/Lib/Library_Ex
+/home/Apple/Libs/Library_Ex

+ 83 - 18
demo/VideoPlayer/VideoPlayer/DecodeVedio.cpp

@@ -94,9 +94,8 @@ void DecodeVedio::stopDecodeVedio()
 
     /* 唤醒阻塞住的解码线程 */
     // /* 等待线程执行结束 */
-    while(m_threadState)
+    while(m_decodeState.load() != DecodeState::DecodeExit)
     {
-        /* 睡眠10ms */
         std::this_thread::sleep_for(std::chrono::milliseconds(5));
     }
     freeAll();
@@ -111,9 +110,14 @@ void DecodeVedio::stopDecodeVedio()
  */
 void DecodeVedio::setCurrentPos(qint64 pos)
 {
+    if(!m_threadRuning)
+    {
+        return;
+    }
     m_isSeek = true;
     /* 先暂停解码 */
     pauseDecode();
+    
     SPDLOG_DEBUG("跳转到:{}ms",pos);
     /*在环形队列中有已解码的视频帧数,需要去掉已有的帧数所占的时间 */
     pos = pos - m_queueImage.QueueSize() * (1000 / m_fps);
@@ -135,8 +139,9 @@ void DecodeVedio::setCurrentPos(qint64 pos)
     /* 刷新解码器缓冲区 */
     m_flushDecoder.store(true);
     /* 清空环形队列中的视频 */
+    SPDLOG_DEBUG("清空环形队列中的视频。");
     QImage* image = 0;
-    for(int i = 0; i < m_queueImage.QueueSize(); i++)
+    while (m_queueImage.QueueSize() > 0)
     {
         image = nullptr;
         m_queueImage.front_pop_NoBlock(image);
@@ -145,6 +150,7 @@ void DecodeVedio::setCurrentPos(qint64 pos)
             delete image;
         }
     }
+    
     /* 继续解码 */
     continueDecode();
 }
@@ -168,6 +174,10 @@ qint64 DecodeVedio::getDuration()
  */
 QImage* DecodeVedio::getOneImage()
 {
+    if(!m_threadRuning)
+    {
+        return nullptr;
+    }
     QImage* image = nullptr;
     if(!m_queueImage.front_pop_NoBlock(image))
     {
@@ -176,12 +186,33 @@ QImage* DecodeVedio::getOneImage()
     return image;
 }
 
-/* 获取一帧图像,直到有图像为止 */
-QImage* DecodeVedio::getOneImageUntilHave()
+/**
+ * @brief 获取一帧图像,直到有图像为止
+ * 
+ * @param timeOut 设为-1是一直等待,设置正整数是等待的时间,单位ms
+ * @return QImage* 
+ */
+QImage* DecodeVedio::getOneImageUntilHave(int timeOut)
 {
-    QImage* image = m_queueImage.front_pop();
-
-    return image;
+    if(!m_threadRuning)
+    {
+        return nullptr;
+    }
+    if(timeOut < 0)
+    {
+        QImage* image = m_queueImage.front_pop();
+        return image;
+    }
+    for(int i = 0; i < timeOut; i++)
+    {
+        QImage* image = nullptr;
+        if(m_queueImage.front_pop_NoBlock(image))
+        {
+            return image;
+        }
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+    return nullptr;
 }
 
 /* 初始化硬件解码器 */
@@ -223,8 +254,8 @@ void DecodeVedio::openVedio(const QString& fileName)
     SPDLOG_DEBUG("开始初始化FFMPEG");
     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 超时,如果等待时间过短,也可能会还没连接就返回了。
+    // av_dict_set(&dict, "max_delay", "3", 0);             // 设置最大复用或解复用延迟(以微秒为单位)。当通过【UDP】 接收数据时,解复用器尝试重新排序接收到的数据包(因为它们可能无序到达,或者数据包可能完全丢失)。这可以通过将最大解复用延迟设置为零(通过max_delayAVFormatContext 字段)来禁用。
+    // av_dict_set(&dict, "timeout", "1000000", 0);         // 以微秒为单位设置套接字 TCP I/O 超时,如果等待时间过短,也可能会还没连接就返回了。
 
     /************ 存储文件格式信息 ************/
     /* 打开文件,读取视频文件的头信息,放在第一个参数的结构体中 */
@@ -362,6 +393,19 @@ void DecodeVedio::openVedio(const QString& fileName)
     m_initFFmpeg = true;
     SPDLOG_INFO("FFMPEG初始化完成!");
     // SPDLOG_INFO("视频宽度:{} 高度:{} 帧率:{} 总时长:{} 总帧数:{}",m_srcSize.width(), m_srcSize.height(), m_fps, m_duration, m_totalFrame);
+    /* 再次判断帧数是否正常,如果没读取到,就使用 总帧数 / 时长 */
+    if(m_fps == 0)
+    {
+        if((m_duration > 0) && (m_totalFrame > 0))
+        {
+            m_fps = m_totalFrame / (m_duration / 1000.0);
+        }
+        /* 到这一步,无法确定帧数了,就按照25帧来计算了 */
+        if(m_fps == 0)
+        {
+            m_fps = 25;
+        }
+    }
 }
 
 
@@ -378,18 +422,19 @@ void DecodeVedio::threadDecodeUsingCPU()
     int retPacket = 0;
     m_pauseDecode = false;
     m_decodeStatus = true;
-    m_threadState = true;
+    m_decodeState.store(DecodeState::DecodeRun);
 
     /* 开始解码 */
     SPDLOG_DEBUG("开始解码...");
     while(m_threadRuning)
     {
         /******** 判断是否在暂停状态 ********/
-        if(m_pauseDecode)
+        while(m_pauseDecode)
         {
             m_decodeState.store(DecodeState::DecodePause);
-            std::this_thread::sleep_for(std::chrono::milliseconds(2));
+            std::this_thread::sleep_for(std::chrono::microseconds(100));
         }
+        m_decodeState.store(DecodeState::DecodeRun);
         /* 刷新解码器缓冲区,清除掉里面残留的解码文件 */
         if(m_flushDecoder.load())
         {
@@ -448,7 +493,7 @@ void DecodeVedio::threadDecodeUsingCPU()
             int ret = avcodec_receive_frame(m_pCodecContext, m_pFrameSRC);
             if(ret == AVERROR_EOF)
             {
-                SPDLOG_INFO("解码完成...");
+                SPDLOG_INFO("读取到视频流末尾。");
                 isEnd = true;
                 break;
             }
@@ -472,7 +517,7 @@ void DecodeVedio::threadDecodeUsingCPU()
             /* 解码成功,获取当前时间,现在已经是准的时间了
              * 如果在跳转状态,在这里判断是否到了目标位置 */
             m_pts = m_pFrameSRC->pts;
-            if(m_isSeek)
+            if(m_isSeek.load())
             {
                 if(m_pts < m_targetPos)
                 {
@@ -539,14 +584,21 @@ void DecodeVedio::threadDecodeUsingCPU()
         av_packet_unref(m_packet);    /* 释放数据包,引用计数-1,为0时释放空间 */
         if(isEnd)
         {
-            break;
+            emit signal_playCompleted();
+            m_decodeState.store(DecodeState::DecodeStop);
+            /* 读取到结尾,但是不退出解码线程,可能还会使用倒退功能,后退读取 */
+            while(m_decodeState.load() != DecodeState::DecodeRun)
+            {
+                std::this_thread::sleep_for(std::chrono::milliseconds(2));
+            }
+            isEnd = false;
         }
     }
 label_ThreadDecodeExit:
     /* 释放空间 */
     av_packet_free(&m_packet);
-
-    m_threadState = false;
+    m_decodeState.store(DecodeState::DecodeExit);
+    m_threadRuning = false;
 }
 
 /* 退出线程,将所有可能暂停线程运行的条件全部唤醒 */
@@ -556,6 +608,8 @@ void DecodeVedio::exitThread()
     {
         m_threadRuning = false;
     }
+    /* 设置成运行状态,唤醒可能阻塞在了解码结束的位置 */
+    m_decodeState.store(DecodeState::DecodeRun);
     m_pauseDecode = false;
     /* 先退出可能阻塞住的解码线程 */
     m_queueImage.exit();
@@ -564,6 +618,17 @@ void DecodeVedio::exitThread()
 /* 暂停解码,会阻塞到线程暂停为止 */
 void DecodeVedio::pauseDecode()
 {
+    if(!m_threadRuning)
+    {
+        return;
+    }
+    if( (m_decodeState.load() == DecodeState::DecodeExit) 
+     || (m_decodeState.load() == DecodeState::DecodePause) )
+    {
+        return;
+    }
+    /* 设置成运行状态,唤醒可能阻塞在了解码结束的位置 */
+    m_decodeState.store(DecodeState::DecodeRun);
     m_pauseDecode = true;
     /* 队列出队两张图片,防止解码线程阻塞到环形队列满上面 */
     QImage* image = nullptr;

+ 16 - 12
demo/VideoPlayer/VideoPlayer/DecodeVedio.h

@@ -21,14 +21,6 @@ extern "C"
 }
 
 
-enum class DecodeState
-{
-    NONE = 0,
-    DecodeRun,          /* 解码运行中 */
-    DecodePause,        /* 暂停解码 */
-    DecodeSeek         /* 跳转中 */
-};
-
 
 /**
  * 使用方式:
@@ -43,6 +35,16 @@ class DecodeVedio : public QObject
 {
     Q_OBJECT
 
+enum class DecodeState
+{
+    NONE = 0,
+    DecodeRun,          /* 解码运行中 */
+    DecodePause,        /* 暂停解码 */
+    DecodeSeek,         /* 跳转中 */
+    DecodeStop,         /* 停止解码,但是并没有退出解码线程 */
+    DecodeExit          /* 退出解码 */
+};
+
 public:
     explicit DecodeVedio(QThread* thread, QObject* parent = nullptr);
     ~DecodeVedio();
@@ -66,15 +68,18 @@ public:
     qint64 getCurrentPos();
     /* 获取视频时长 */
     qint64 getDuration();
+    qint64 getTotalFrame() { return m_totalFrame; }
     
 
-    /* 获取一帧图像 */                  
+    /* 获取一帧图像 */
     QImage* getOneImage();
-    /* 获取一帧图像,直到有图像为止 */                          
-    QImage* getOneImageUntilHave();                 
+    /* 获取一帧图像,直到有图像为止,可以设置超时时间 */
+    QImage* getOneImageUntilHave(int timeOut = -1);
 
     /* 获取帧数 */
     int getFPS() const { return m_fps; }
+    /* 设置帧数 */
+    void setFPS(int fps) { m_fps = fps; }
     /* 获取图像宽度 */
     QSize getSrcVideoSize() const {return m_srcSize; }
     /* 获取解码器名称(编码格式) */
@@ -109,7 +114,6 @@ private:
     /* 线程状态 */
     std::atomic_bool m_threadRuning = false;        /* 解码线程是运行标志 */
     std::atomic_bool m_initFFmpeg = false;          /* ffmpeg初始化标志 */
-    std::atomic_bool m_threadState = false;         /* 线程运行状态 */
     std::atomic_bool m_pauseDecode = false;         /* 暂停解码 */
     std::atomic_bool m_decodeStatus = false;        /* 解码状态,这里主要是检测是否暂停解码 */
     std::atomic_bool m_isSeek = false;              /* 是否在跳转中 */

+ 101 - 28
demo/VideoPlayer/VideoPlayer/VideoPlayer.cpp

@@ -17,15 +17,17 @@ VideoPlayer::VideoPlayer(QWidget *parent) : QWidget(parent)
     // m_threadDecode = new QThread(this);
     // m_decodeVedio = new DecodeVedio(m_threadDecode);
 
+    m_previewImage = 2;
+    m_fps = 0;
+
     m_semRefresh = new QSemaphore(0);
 
     m_timerRefreshUI.setSingleShot(false);
     /* 设置精度毫秒级 */
     m_timerRefreshUI.setTimerType(Qt::PreciseTimer);
     connect(&m_timerRefreshUI, &QTimer::timeout, this, &VideoPlayer::do_refreshUI);
-
     connect(this, &VideoPlayer::signal_refreshImage, this, &VideoPlayer::do_refreshSamImage);
-    connect(m_decodeVedio, &DecodeVedio::signal_playCompleted, this, &VideoPlayer::do_playCompleted);
+
     SPDLOG_TRACE("UI线程ID:{}", QThread::currentThreadId());
 
 }
@@ -62,6 +64,7 @@ void VideoPlayer::openPlayVedio(const QString& fileName)
         /* 初始化解码线程 */
         m_threadDecode = new QThread(this);
         m_decodeVedio = new DecodeVedio(m_threadDecode);
+        connect(m_decodeVedio, &DecodeVedio::signal_playCompleted, this, &VideoPlayer::do_playCompleted);
     }
     if(m_decodeVedio->isDecoding())
     {
@@ -81,12 +84,13 @@ void VideoPlayer::openPlayVedio(const QString& fileName)
     m_srcHeight = m_decodeVedio->getSrcVideoSize().height();
     m_fps = m_decodeVedio->getFPS();
     m_duration = m_decodeVedio->getDuration();
+    auto totalFarame = m_decodeVedio->getTotalFrame();
     SPDLOG_INFO("视频编码格式:{}", m_decodeVedio->getDecoderName().toStdString());
     int hh = m_duration / 3600000;
     int mm = (m_duration % 3600000) / 60000;
     int ss = (m_duration % 60000) / 1000;
     int ms = m_duration % 1000;
-    SPDLOG_INFO("视频分辨率:{}x{} 帧数:{}", m_srcWidth, m_srcHeight, m_fps);
+    SPDLOG_INFO("视频分辨率:{}x{} 帧率:{} 总帧数:{}", m_srcWidth, m_srcHeight, m_fps, totalFarame);
     SPDLOG_INFO("时长:{}h:{}m:{}.{}s 总时长:{}ms", hh, mm, ss, ms, m_duration);
 
     /* 设置视频宽和高的最小大小 */
@@ -119,7 +123,7 @@ bool VideoPlayer::play()
     /* 设置刷新时间 */
     m_timerRefreshUI.setSingleShot(false);
     m_interval = qRound64(1000.0 / m_fps);
-    SPDLOG_TRACE("刷新UI的定时间隔:{}",m_interval);
+    SPDLOG_DEBUG("刷新UI的定时间隔:{}",m_interval);
     m_timerRefreshUI.start(m_interval);
     m_playStatus = true;
     
@@ -191,12 +195,7 @@ void VideoPlayer::backward(qint64 ms)
     }
 
     setCurrentPos(pos);
-    /* 如果是暂停状态,就刷新两帧显示跳转结果 */
-    if(!m_playStatus)
-    {
-        m_semRefresh->release(2);
-        emit signal_refreshImage();
-    }
+
 }
 
 /* 前进,单位ms */
@@ -213,12 +212,7 @@ void VideoPlayer::forward(qint64 ms)
     pos = pos + ms;
     
     setCurrentPos(pos);
-    /* 如果是暂停状态,就刷新两帧显示跳转结果 */
-    if(!m_playStatus)
-    {
-        m_semRefresh->release(2);
-        emit signal_refreshImage();
-    }
+
 }
 
 /* 获取视频时长 */
@@ -272,7 +266,8 @@ void VideoPlayer::setCurrentPos(qint64 pos)
     }else
     {
         /* 刷新2张照片 */
-        m_semRefresh->release(2);
+        m_semRefresh->release(m_previewImage);
+        emit signal_refreshImage();
     }
 }
 
@@ -320,24 +315,69 @@ void VideoPlayer::setPlayWidgetSize(int width,int height)
  * @brief 设置播放窗口,这用于独占一个传入的widget,这里会自动添加一个布局,外面窗口变化,这里也跟随着变化
  * 
  * @param widget 
+ * @param flag 
+ *  @arg true:独占widget,并设置一个layout,会随着传入的widget大小变化
+ *  @arg false:不独占
  */
-void VideoPlayer::setPlayWidget(QWidget* widget)
+void VideoPlayer::setPlayWidget(QWidget* widget, bool flag)
 {
     if(widget == nullptr)
     {
         SPDLOG_WARN("传入的widget为空");
         return;
     }
-    /* 设置布局 */
-    QVBoxLayout* layout = new QVBoxLayout(widget);
-    layout->addWidget(this);
-    layout->setMargin(0);
-    layout->setSpacing(0);
-    widget->setLayout(layout);
-    /* 设置窗口大小 */
-    setPlayWidgetSize(widget->width(), widget->height());
+    if(flag)
+    {
+        /* 设置布局 */
+        QVBoxLayout* layout = new QVBoxLayout(widget);
+        layout->addWidget(this);
+        layout->setMargin(0);
+        layout->setSpacing(0);
+        widget->setLayout(layout);
+    }else 
+    {
+        this->setParent(widget);
+        /* 设置窗口大小 */
+        setPlayWidgetSize(widget->width(), widget->height());
+    }
+    
+}
+
+
+/**
+ * @brief 设置预览图片数目,在暂停时跳转,可能会有花屏或者黑帧,可以设置跳转图片个数跳过黑帧
+ *        默认是2帧
+ * 
+ * @param num 
+ */
+void VideoPlayer::setPreviewImage(int num)
+{
+    m_previewImage = num;
+}
+
+/**
+ * @brief 设置帧率,有些视频无法获取到帧率,就会使用默认的25fps,如果需要,可以通过这个函数设置
+ *        注意:这个函数需要在打开视频文件之后设置,打开一次视频文件会覆盖这个参数
+ * 
+ * @param fps 
+ */
+void VideoPlayer::setFPS(int fps)
+{
+    m_fps = fps;
+    if(m_decodeVedio != nullptr)
+    {
+        m_decodeVedio->setFPS(fps);
+    }
+    if(m_timerRefreshUI.isActive())
+    {
+        m_timerRefreshUI.stop();
+        m_interval = qRound64(1000.0 / m_fps);
+        m_timerRefreshUI.start(m_interval);
+    }
 }
 
+
+
 /* 列出当前环境支持的硬件解码器 */
 void VideoPlayer::ListHWDecoder()
 {
@@ -385,6 +425,7 @@ void VideoPlayer::ListHWDecoder()
     // }
 }
 
+
 /* 设置播放回调函数 */
 // void VideoPlayer::setPlayCallBack(std::function<Play_CallBack> playCallBack,void* context)
 // {
@@ -493,6 +534,10 @@ void VideoPlayer::do_refreshUI()
 /* 通过信号刷新第一张图片 */
 void VideoPlayer::do_refreshSamImage()
 {
+    if(!m_isOpenFile)
+    {
+        return;
+    }
     while(m_semRefresh->tryAcquire(1))
     {
         /* 取出第一张 */
@@ -505,8 +550,8 @@ void VideoPlayer::do_refreshSamImage()
                 delete m_image;
                 m_image = nullptr;
             }
-            m_image = m_decodeVedio->getOneImageUntilHave();
-            
+            /* 等待图片,最多等待50ms */
+            m_image = m_decodeVedio->getOneImageUntilHave(100);
             if(m_image)
             {
                 if(m_image->isNull())
@@ -526,6 +571,34 @@ void VideoPlayer::do_playCompleted()
 {
     SPDLOG_INFO("视频播放完成。");
     m_timerRefreshUI.stop();
+    /* 手动刷新剩余的环形队列中的图片 */
+    while(true)
+    {
+        if(m_decodeVedio != nullptr)
+        {
+            QImage* image = nullptr;
+            image = m_decodeVedio->getOneImage();
+            if(image == nullptr)
+            {
+                break;
+            }
+            /* 删除上一帧图片 */
+            if(m_image != nullptr)
+            {
+                delete m_image;
+                m_image = nullptr;
+            }
+            m_image = image;
+            
+            if(m_image->isNull())
+            {
+                SPDLOG_WARN("取出的图片为空...");
+                return;
+            }
+            // SPDLOG_DEBUG("绘制画面...");
+            update();
+        }
+    }
     m_playStatus = false;
     // if(m_funcPlayCB != nullptr)
     // {

+ 7 - 2
demo/VideoPlayer/VideoPlayer/VideoPlayer.h

@@ -26,8 +26,11 @@ public:
     qint64 getDuration();                           /* 获取视频时长 */
     qint64 getCurrentPos();                         /* 获取当前播放位置 */
     void setCurrentPos(qint64 pos);                 /* 设置当前播放位置 */
+
     void setPlayWidgetSize(int width,int height);   /* 设置播放视频窗口的大小 */
-    void setPlayWidget(QWidget* widget);            /* 设置播放窗口 */
+    void setPlayWidget(QWidget* widget, bool flag = false); /* 设置播放窗口 */
+    void setPreviewImage(int num = 2);              /* 设置预览图片数目,在暂停时跳转,可能会有花屏或者黑帧,可以设置跳转图片个数跳过黑帧 */
+    void setFPS(int fps);                            /* 设置帧率 */
 
     static void ListHWDecoder();                    /* 列出当前环境支持的硬件解码器 */
 
@@ -66,8 +69,10 @@ private:
     bool m_isOpenFile = false;                      /* 是否打开了视频文件,未打开视频文件也就是未初始化解码线程 */
     QSemaphore* m_semRefresh = nullptr;             /* 刷新信号量 */
 
+    int m_previewImage = 0;                         /* 预览图片数目 */
+
     // std::function<Play_CallBack> m_funcPlayCB = nullptr;  /* 播放回调函数 */
-    void* m_context = nullptr;                      /* 上下文 */
+    // void* m_context = nullptr;                      /* 上下文 */
 };
 
 

+ 477 - 0
demo/VideoPlayer/demo/NetworkVideoDecode.cpp

@@ -0,0 +1,477 @@
+#include "NetworkVideoDecode.h"
+
+
+#include "videodecode.h"
+#include <QDebug>
+#include <QDir>
+#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];
+}
+
+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;
+    }
+}
+
+/**
+ * @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;
+    m_strCodecName = codec->name;
+
+#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线程解码
+
+    // 初始化解码器上下文,如果之前avcodec_alloc_context3传入了解码器,这里设置NULL就可以
+    ret = avcodec_open2(m_codecContext, nullptr, nullptr);
+    if(ret < 0)
+    {
+        showError(ret);
+        free();
+        return false;
+    }
+
+    // 分配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;
+    }
+
+    // 分配图像空间
+    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 openSave();
+}
+
+/**
+ * @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(m_formatContextSave)
+            {
+                // 由于保存的m_formatContextSave只创建了一个视频流,而读取到的图像的流索引不一定为0,可能会出现错误【Invalid packet stream index: 1】
+                // 所以这里需要将stream_index指定为和m_formatContextSave中视频流索引相同,因为就一个流,所以直接设置为0
+                m_packet->stream_index = 0;
+                av_write_frame(m_formatContextSave, m_packet);   // 将数据包写入输出媒体文件
+            }
+            // 计算当前帧时间(毫秒)
+#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();
+    }
+
+    m_pts = m_frame->pts;
+
+    // 为什么图像转换上下文要放在这里初始化呢,是因为m_frame->format,如果使用硬件解码,解码出来的图像格式和m_codecContext->pix_fmt的图像格式不一样,就会导致无法转换为QImage
+    if(!m_swsContext)
+    {
+        // 获取缓存的图像转换上下文。首先校验参数是否一致,如果校验不通过就释放资源;然后判断上下文是否存在,如果存在直接复用,如不存在进行分配、初始化操作
+        m_swsContext = sws_getCachedContext(m_swsContext,
+                                            m_frame->width,                     // 输入图像的宽度
+                                            m_frame->height,                    // 输入图像的高度
+                                            (AVPixelFormat)m_frame->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_frame->data,            // 原图像数组
+                    m_frame->linesize,        // 包含源图像每个平面步幅的数组
+                    0,                        // 开始位置
+                    m_frame->height,          // 行数
+                    data,                     // 目标图像数组
+                    lines);                   // 包含目标图像每个平面的步幅的数组
+    QImage image(m_buffer, m_frame->width, m_frame->height, QImage::Format_RGBA8888);
+    av_frame_unref(m_frame);
+
+    return image;
+}
+
+/**
+ * @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        显示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()
+{
+    if(m_formatContextSave && m_writeHeader)
+    {
+        av_write_trailer(m_formatContextSave);   // 写入文件尾
+        m_writeHeader = false;
+
+        avformat_free_context(m_formatContextSave);
+        m_formatContext = nullptr;
+        m_videoStream = nullptr;
+    }
+    // 因为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(m_packet)
+    {
+        av_packet_free(&m_packet);
+    }
+    if(m_frame)
+    {
+        av_frame_free(&m_frame);
+    }
+    if(m_buffer)
+    {
+        delete [] m_buffer;
+        m_buffer = nullptr;
+    }
+}
+
+/**
+ * @brief  打开输出文件
+ * @return
+ */
+bool VideoDecode::openSave()
+{
+    QDir dir;
+    if(!dir.exists("./Videos"))
+    {
+        dir.mkdir("./Videos");
+    }
+    QString strName = QString("./Videos/%1.h264").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH-mm-ss"));
+    int ret = avformat_alloc_output_context2(&m_formatContextSave, nullptr, m_strCodecName.toStdString().data(), strName.toStdString().data());  // 这里使用和解码一样的编码器,防止保存的图像颜色出问题
+
+    if(ret < 0)
+    {
+        free();
+        showError(ret);
+        return false;
+    }
+    // 创建并初始化AVIOContext以访问url所指示的资源。
+    ret = avio_open(&m_formatContextSave->pb, strName.toStdString().data(), AVIO_FLAG_WRITE);
+    if(ret < 0)
+    {
+        free();
+        showError(ret);
+        return false;
+    }
+
+    // 向媒体文件添加新流
+    m_videoStream = avformat_new_stream(m_formatContextSave, nullptr);
+    if(!m_videoStream)
+    {
+        free();
+        showError(AVERROR(ENOMEM));
+        return false;
+    }
+
+    //拷贝一些参数,给codecpar赋值(这里使用编码器上下文进行赋值)
+    ret = avcodec_parameters_from_context(m_videoStream->codecpar, m_codecContext);
+    if(ret < 0)
+    {
+        free();
+        showError(ret);
+        return false;
+    }
+
+    // 写入文件头
+    ret = avformat_write_header(m_formatContextSave, nullptr);
+    if(ret < 0)
+    {
+        free();
+        showError(ret);
+        return false;
+    }
+    m_writeHeader = true;
+
+    qDebug() << "开始录制视频!";
+    return true;
+}
+

+ 63 - 0
demo/VideoPlayer/demo/NetworkVideoDecode.h

@@ -0,0 +1,63 @@
+
+#ifndef VIDEODECODE_H
+#define VIDEODECODE_H
+
+#include <QString>
+#include <QSize>
+
+struct AVFormatContext;
+struct AVCodecContext;
+struct AVRational;
+struct AVPacket;
+struct AVFrame;
+struct SwsContext;
+struct AVBufferRef;
+struct AVStream;
+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();                          // 获取当前帧显示时间
+
+private:
+    void initFFmpeg();                            // 初始化ffmpeg库(整个程序中只需加载一次)
+    void showError(int err);                      // 显示ffmpeg执行错误时的错误信息
+    qreal rationalToDouble(AVRational* rational); // 将AVRational转换为double
+    void clear();                                 // 清空读取缓冲
+    void free();                                  // 释放
+    bool openSave();                              // 打开输出文件并初始化
+
+private:
+    AVFormatContext* m_formatContext = nullptr;   // 解封装上下文
+    AVCodecContext*  m_codecContext  = nullptr;   // 解码器上下文
+    SwsContext*      m_swsContext    = nullptr;   // 图像转换上下文
+    AVPacket* m_packet = nullptr;                 // 数据包
+    AVFrame*  m_frame  = 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;                    // YUV图像需要转换位RGBA图像,这里保存转换后的图形数据
+
+    /********  保存裸流使用 ******************/
+    AVFormatContext* m_formatContextSave = nullptr;  // 封装上下文
+    QString m_strCodecName;                          // 编解码器名称
+    AVStream* m_videoStream = nullptr;               // 输出视频流
+    bool m_writeHeader = false;                      // 是否写入文件头
+};
+
+#endif // VIDEODECODE_H
+

+ 9 - 1
demo/VideoPlayer/widget.cpp

@@ -17,7 +17,7 @@ Widget::Widget(QWidget *parent)
     ui->setupUi(this);
 
     m_videoPlayer = std::make_shared<VideoPlayer>();
-    m_videoPlayer->setPlayWidget(ui->widget_display);
+    m_videoPlayer->setPlayWidget(ui->widget_display, true);
     
 
     // m_videoPlayer1 = std::make_shared<VideoPlayer1>();
@@ -56,6 +56,14 @@ void Widget::on_pBtn_openVideo_clicked()
     
 }
 
+/* 打开网络视频流 */
+void Widget::on_pBtn_openStream_clicked()
+{
+    SPDLOG_INFO("点击了“打开网络视频流”按钮");
+    m_videoPlayer->openPlayVedio("rtsp://admin:LH123456@192.1.2.73:554/Streaming/Channels/101");
+    // m_videoPlayer1->setPlayVedio("rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov");
+}
+
 void Widget::on_pBtn_play_clicked()
 {
     SPDLOG_INFO("点击了“播放”按钮");

+ 2 - 0
demo/VideoPlayer/widget.h

@@ -21,6 +21,8 @@ private slots:
 
     void on_pBtn_openVideo_clicked();
 
+    void on_pBtn_openStream_clicked();
+
     void on_pBtn_play_clicked();
 
     void on_pBtn_pause_clicked();

+ 17 - 10
demo/VideoPlayer/widget.ui

@@ -29,10 +29,10 @@
       </size>
      </property>
      <layout class="QGridLayout" name="gridLayout">
-      <item row="3" column="0" colspan="2">
+      <item row="6" column="0" colspan="2">
        <widget class="QLineEdit" name="lineEdit"/>
       </item>
-      <item row="2" column="1">
+      <item row="3" column="1">
        <widget class="QPushButton" name="pBtn_stop">
         <property name="text">
          <string>停止</string>
@@ -40,9 +40,9 @@
        </widget>
       </item>
       <item row="1" column="1">
-       <widget class="QPushButton" name="pBtn_pause">
+       <widget class="QPushButton" name="pBtn_play">
         <property name="text">
-         <string>暂停</string>
+         <string>播放</string>
         </property>
        </widget>
       </item>
@@ -53,24 +53,31 @@
         </property>
        </widget>
       </item>
+      <item row="2" column="1">
+       <widget class="QPushButton" name="pBtn_pause">
+        <property name="text">
+         <string>暂停</string>
+        </property>
+       </widget>
+      </item>
       <item row="0" column="1">
-       <widget class="QPushButton" name="pBtn_play">
+       <widget class="QPushButton" name="pBtn_openStream">
         <property name="text">
-         <string>播放</string>
+         <string>打开网络视频</string>
         </property>
        </widget>
       </item>
       <item row="1" column="0">
-       <widget class="QPushButton" name="pBtn_backward">
+       <widget class="QPushButton" name="pBtn_forward">
         <property name="text">
-         <string>后退</string>
+         <string>前进</string>
         </property>
        </widget>
       </item>
       <item row="2" column="0">
-       <widget class="QPushButton" name="pBtn_forward">
+       <widget class="QPushButton" name="pBtn_backward">
         <property name="text">
-         <string>前进</string>
+         <string>后退</string>
         </property>
        </widget>
       </item>