(四)qt中使用ffmpeg播放视频,可暂停恢复

一、在qt中添加ffmpeg库及头文件

INCLUDEPATH += /usr/local/ffmpeg/include
LIBS += -L/usr/local/lib -lavutil -lavcodec -lavformat -lswscale

二、详细代码

FFempegVideoDecode 视频解码类(放入线程中)

ffmpegvideodecode.h

#ifndef FFMPEGVIDEODECODE_H
#define FFMPEGVIDEODECODE_H

#include <QObject>
#include <QThread>
#include <QDebug>
#include <QTime>
struct AVCodec;
struct AVCodecContext;
struct AVFrame;
struct AVFormatContext;
struct SwsContext;
struct AVPacket;

class FFmpegVideoDecode : public QObject
{
    Q_OBJECT
public:
    explicit FFmpegVideoDecode(QObject *parent = nullptr);
    ~FFmpegVideoDecode();

    bool initFFmpeg(QString fileName);
    void clear();

private:
    void inputError(int ret, QString funName);

signals:
    void sigToStart();
    void sigToUpdateImage(const QImage &image);

public slots:
    void onStartPlay(QString name);
    void onStopPlay();
    void onFinish();
    void onUpdateRead();

private:
    AVFormatContext* pFormatCtx = nullptr;
    AVCodecContext* pCodecCtx = nullptr;
    AVFrame* pAvFrame = nullptr;
    AVFrame* pFrameRGB32 = nullptr;
    AVPacket* packet = nullptr;
    uint8_t *out_buffer = nullptr;
    SwsContext *img_convert_ctx = nullptr;
    int videoIndex;
    bool is_stop = false;
    bool is_finish = true;
};

#endif // FFMPEGVIDEODECODE_H

ffmpegvideodecode.cpp

#include "ffmpegvideodecode.h"
extern "C"
{
#include "libavutil/avutil.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
#include <QEventLoop>
#include <QPixmap>
#include <QTimer>

/**
 * @brief      非阻塞延时
 * @param msec 延时毫秒
 */
void  sleepMsec(int msec)
{
    if(msec <= 0) return;
    QEventLoop loop;		//定义一个新的事件循环
    QTimer::singleShot(msec, &loop, SLOT(quit()));//创建单次定时器,槽函数为事件循环的退出函数
    loop.exec();			//事件循环开始执行,程序会卡在这里,直到定时时间到,本循环被退出
}

FFmpegVideoDecode::FFmpegVideoDecode(QObject *parent) : QObject(parent)
{
    connect(this,&FFmpegVideoDecode::sigToStart,this,&FFmpegVideoDecode::onUpdateRead);
}

FFmpegVideoDecode::~FFmpegVideoDecode()
{
    clear();
}

bool FFmpegVideoDecode::initFFmpeg(QString fileName)
{
    clear();

    /*
     * avformat_network_init主要就是初始化win socket和openssl。但是由于我使用linux(ubuntu),未使用openssl支持,所以该函数对我平台没有任何意义。
     * 执行网络库的全局初始化。这是可选的,不再推荐。此函数仅用于解决旧GnuTLS或OpenSSL库的线程安全问题。
       如果libavformat链接到这些库的较新版本,或者不使用它们,则不需要调用此函数。否则,您需要在启动使用该函数的任何其他线程之前调用该函数。
       一旦删除对旧GnuTLS和OpenSSL库的支持,此函数将被弃用,并且此函数没有任何用途
    *.
    //avformat_network_init();


    /*
     * int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
     * AVDictionary是FFmpeg中用于存储元数据的一种数据结构,它基本上是一个简单的哈希表,用于存储字符串类型的键值对。它可以被用来传递多种数据,例如视频解码参数、音频参数、字幕参数等等。
     * 其中,pm是要添加键值对的AVDictionary对象的指针指针;key是要添加的键名;value是要添加的键值;flags是一些标志位,可选项有:
       0:默认,如果已经定义一个关键字,则将其删除并替换为新的键值对。
       AV_DICT_APPEND:如果关键字已经定义,则将其与新的值合并。
       AV_DICT_DONT_OVERWRITE:如果已经存在键名,则不会执行任何操作。
    */
    AVDictionary* dict = nullptr;
    //使用 TCP 方式
    av_dict_set(&dict, "rtsp_transport", "tcp", 0);
    //设置 接收包间隔最大延迟,微秒
    av_dict_set(&dict, "max_delay", "200", 0);
    //在进行网络操作时允许的最大等待时间。5秒
    av_dict_set(&dict, "timeout", "5000000", 0);
    //设置阻塞超时,否则可能在流断开时连接发生阻塞,微秒
    av_dict_set(&dict, "stimeout", "3000000", 0);
    //设置 find_stream_info 最大时长,微秒
    //av_dict_set(&dict, "analyzeduration", "1000000", 0);

    /*
     * avformat_open_input 打开输入媒体流
     * 参数ps包含一切媒体相关的上下文结构,有它就有了一切,本函数如果打开媒体成功,
       会返回一个AVFormatContext的实例.
       参数url媒体文件名或URL.
       参数fmt是要打开的媒体格式的操作结构,因为是读,所以是inputFormat.此处可以
       传入一个调用者定义的inputFormat,对应命令行中的 -f xxx段,如果指定了它,
       在打开文件中就不会探测文件的实际格式了,以它为准了.
       参数options是对某种格式的一些操作,是为了在命令行中可以对不同的格式传入
       特殊的操作参数而建的, 为了了解流程,完全可以无视它.
    */
    /*
     * avformat_alloc_context用于分配一个AVFormatContext结构体并初始化它
    */
    pFormatCtx = avformat_alloc_context();
    int ret = avformat_open_input(&pFormatCtx,
                                  fileName.toStdString().c_str(),
                                  NULL,
                                  &dict);
    // 释放参数字典
    if(dict)
        av_dict_free(&dict);

    if (ret != 0)
    {
        inputError(ret,"avformat_open_input");
        clear();
        return false;
    }

    /*
     * 用于找到媒体文件中的流信息,也就是获取视频和音频的相关数据,如编码格式、分辨率、采样率等
     * 获取多媒体流的信息(视频文件信息),一个视频文件中可能会同时包括视频文件、音频文件、字幕文件等多个媒体流。
    */
    ret = avformat_find_stream_info(pFormatCtx, NULL);
    if (ret < 0)
    {
        inputError(ret,"avformat_find_stream_info");
        clear();
        return false;
    }

    qint64 totalTime = pFormatCtx->duration / (AV_TIME_BASE / 1000); // 计算视频总时长(毫秒)
    qDebug() << QString("视频总时长:%1 ms,[%2]").arg(totalTime).arg(QTime::fromMSecsSinceStartOfDay(int(totalTime)).toString("HH:mm:ss zzz"));

    /*
     * 通过AVMediaType枚举查询视频流ID(也可以通过遍历查找)
    */
    /*
     * int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);
     * 查找最佳匹配的媒体流的函数
     * ic:AVFormatContext指针,表示输入的媒体文件上下文。
      type:要查找的媒体流类型,可以是音频流、视频流或字幕流等。
      wanted_stream_nb:期望的媒体流索引号,可以是特定的索引号,也可以是AV_NOPTS_VALUE(-1)表示任意流。
      related_stream:前一个相关流的索引号,如果没有前一个相关流,则传入-1。
      decoder_ret:返回解码器指针。
      flags:查找最佳流的标志位,默认为0。
      返回值:
      找到的最佳匹配媒体流的索引号,如果找不到则返回AVERROR_STREAM_NOT_FOUND。
    */
    videoIndex = -1;
    videoIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    /*
    for (int i = 0; i < pFormatCtx->nb_streams; ++i)
    {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoIndex = i;
            break;
        }
    }
    */
    if (videoIndex < 0)
    {
        inputError(videoIndex,"av_find_best_stream");
        clear();
        return false;
    }

    /*
     * AVCodecParameters 用于保存音视频流的基本参数信息。
     * 主要成员说明
     * codec_id:编解码器的ID,取值为枚举类型AVCodecID中的一种。
     * width:视频帧宽度。
     * height:视频帧高度。
    */
    AVCodecParameters* parmeter = pFormatCtx->streams[videoIndex]->codecpar;
    /*
     * avcodec_find_decoder 通过解码器ID获取视频解码器(新版本返回值必须使用const)
    */
    const AVCodec* _pCodec = avcodec_find_decoder(parmeter->codec_id);
    if (_pCodec == NULL)
    {
        inputError(AVERROR_DECODER_NOT_FOUND,"avcodec_find_decoder");
        clear();
        return false;
    }

    /*
     * avcodec_alloc_context3 用于分配一个编解码器上下文(AVCodecContext)结构体
     * AVCodecContext结构的主要作用是设置编码过程的参数
    */
    pCodecCtx = avcodec_alloc_context3(_pCodec);//初始化一个编解码上下文

    /*
     * avcodec_parameters_to_context 该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中
    */
    ret = avcodec_parameters_to_context(pCodecCtx, parmeter);
    if (ret < 0)
    {
        inputError(ret,"avcodec_parameters_to_context");
        clear();
        return false;
    }

    /*
     * avcodec_open2 用于打开编解码器并初始化其上下文
    */
    ret = avcodec_open2(pCodecCtx, _pCodec, NULL);
    if (ret < 0)
    {
        inputError(ret,"avcodec_open2");
        clear();
        return false;
    }


    //分配AVFrame并将其字段设置为默认值。
    pAvFrame = av_frame_alloc();
    pFrameRGB32 = av_frame_alloc(); //存储解码后转换的RGB数据

    //保存RGB32
    /*
     * av_image_get_buffer_size 用于计算图像缓冲区的大小
    */
    /*
     * av_image_fill_arrays 用于将图像数据填充到AVFrame结构体中,瓜分malloc分配到的内存
     * 参数const uint8_t *src之后的逻辑中就没再用了->对分配内存瓜分
    */
    int size = av_image_get_buffer_size(AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height,4);
    out_buffer = (uint8_t *)av_malloc(size);
    ret = av_image_fill_arrays(pFrameRGB32->data,pFrameRGB32->linesize, out_buffer, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height,1);
    if (ret < 0)
    {
        inputError(ret,"av_image_fill_arrays");
        clear();
        return false;
    }

    //分配AVPacket并将其字段设置为默认值。
    packet = av_packet_alloc();
    /*
     * av_new_packet 用于分配一个新的AVPacket结构体
    */
    av_new_packet(packet, pCodecCtx->width * pCodecCtx->height);//分配packet的有效载荷并初始化其字段

    //qDebug() << "***********视频信息**********";
    //av_dump_format(pFormatCtx, 0, fileName.toStdString().c_str(), 0);
    //qDebug() << "*****************************";
    return true;
}

void FFmpegVideoDecode::onStartPlay(QString name)
{
    bool flag = initFFmpeg(name);
    is_stop = false;
    if(flag)
    {
        is_finish = false;
        Q_EMIT sigToStart();
    }
    else
    {
        qDebug()<<"初始化ffmpeg失败!";
    }
}

void FFmpegVideoDecode::onStopPlay()
{
    is_stop = !is_stop;
}

void FFmpegVideoDecode::onFinish()
{
    qDebug()<<"关闭视频";
    is_finish = true;
}

void FFmpegVideoDecode::clear()
{
    if(pFrameRGB32)
    {
        av_frame_free(&pFrameRGB32);
        pFrameRGB32 = nullptr;
    }
    if(pAvFrame)
    {
        av_frame_free(&pAvFrame);
        pAvFrame = nullptr;
    }
    if(pCodecCtx)
    {
        avcodec_close(pCodecCtx);
        pCodecCtx = nullptr;
    }
    // 因为avformat_flush不会刷新AVIOContext (s->pb)。如果有必要,在调用此函数之前调用avio_flush(s->pb)。
    if(pFormatCtx && pFormatCtx->pb)
    {
        avio_flush(pFormatCtx->pb);
    }
    if(pFormatCtx)
    {
        avformat_flush(pFormatCtx);   // 清理读取缓冲
        avformat_close_input(&pFormatCtx);
        pFormatCtx = nullptr;
    }
    if (img_convert_ctx)
    {
        sws_freeContext(img_convert_ctx);
        img_convert_ctx = nullptr;
    }
    if(out_buffer)
    {
        //delete [] out_buffer;
        av_free(out_buffer);
        out_buffer = nullptr;
    }
}

void FFmpegVideoDecode::inputError(int ret, QString funName)
{
    char *error = new char[1024];
    memset(error, 0, 1024);// 将数组置零

    av_strerror(ret, error, 1024);
    qDebug()<<"函数名为:"<<funName<<" 的函数发生错误,错误原因:"<<QString::fromStdString(error)<<" 返回值:"<<ret;

    delete[] error;
    error = NULL;
}

void FFmpegVideoDecode::onUpdateRead()
{
    //设置sws_scale转换格式为AV_PIX_FMT_RGB32
    /*
     * sws_getContext 用于创建一个SWSContext结构体
     * struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
                                  int flags, SwsFilter *srcFilter,
                                  SwsFilter *dstFilter, const double *param);
     * 参数srcW和srcH分别表示源图像的宽度和高度
     * 参数srcFormat表示源图像的像素格式
     * 参数dstFormat表示目标图像的像素格式
     * 参数flags表示转换的标志;
     * 参数srcFilter和dstFilter分别表示源图像和目标图像的滤波器;
     * 参数param表示转换的参数。
    */
    img_convert_ctx = sws_getContext(pCodecCtx->width,
                                     pCodecCtx->height,
                                     pCodecCtx->pix_fmt,
                                     pCodecCtx->width,
                                     pCodecCtx->height,
                                     AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);

    while (!is_finish)
    {
        //方法1:由于子线程阻塞式延时读取视频帧,无法修改视频的暂停状态,可通过修改connect的连接方式在主线程中修改暂停状态
        if (is_stop)
        {
            continue;
        }
        //方法2:将子线程阻塞式延时修改未非阻塞式,可直接修改视频的暂停状态
//        while (is_stop && !is_finish)
//        {
//            sleepMsec(200);
//        }

        /*
         * int av_read_frame(AVFormatContext *s, AVPacket *pkt);用于从输入流中读取一帧数据。
         * 参数s是一个指向AVFormatContext指针的指针,表示要读取数据的输入流;
         * 参数pkt是一个指向AVPacket指针的指针,表示要存储读取到的数据的AVPacket结构体。
        */
        if (av_read_frame(pFormatCtx, packet) >= 0)
        {
            if (videoIndex == packet->stream_index) //此流是视频流
            {
                /*
                 * int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);用于将AVPacket结构体中的数据发送到解码器。
                 * 参数avctx是一个指向AVCodecContext指针的指针,表示要发送数据的解码器;
                 * 参数avpkt是一个指向AVPacket指针的指针,表示要发送的数据。
                */
                /*
                 * int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);用于从解码器中接收解码后的帧数据。
                 * 参数avctx是一个指向AVCodecContext指针的指针,表示要接收数据的解码器;
                 * 参数frame是一个指向AVFrame指针的指针,表示要存储接收到的数据的AVFrame结构体。
                */
                int ret = avcodec_send_packet(pCodecCtx, packet);
                if (ret < 0)
                {
                    inputError(ret,"avcodec_send_packet");
                    continue;
                }
                ret = avcodec_receive_frame(pCodecCtx, pAvFrame);
                if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                {
                    //拉流显示视频时一开始报错: 返回值:-11,资源暂时不可用
                    //代表解码器暂时没有数据可读,需要输入更多的AVPacket。
                    //在代码中设定碰到这个错误时直接continue让avcodec_send_packet()对解码器输入更多数据
                    continue;
                }
                else if(ret < 0)
                {
                    inputError(ret,"avcodec_receive_frame");
                    continue;
                }

                /*
                 * sws_scale 用于将图像从一个像素格式转换为另一个像素格式。
                 * 可以在同一个函数里实现:1.图像色彩空间转换, 2:分辨率缩放,3:前后图像滤波处理
                */
                sws_scale(img_convert_ctx,
                          (const uint8_t* const*)pAvFrame->data,
                          pAvFrame->linesize,
                          0,
                          pCodecCtx->height,
                          pFrameRGB32->data,
                          pFrameRGB32->linesize);
                QImage image((uchar*)pFrameRGB32->data[0], pCodecCtx->width, pCodecCtx->height, QImage::Format_RGB32);
                Q_EMIT sigToUpdateImage(image);
                //sleepMsec->非阻塞(点击播放可直接重头播放) QThread::msleep(40)->阻塞
                //方法1:由于子线程阻塞式延时读取视频帧,无法修改视频的暂停状态,可通过修改connect的连接方式在主线程中修改暂停状态
                QThread::msleep(40);
                //方法2:将子线程阻塞式延时修改未非阻塞式,可直接修改视频的暂停状态
//                sleepMsec(40);
            }
            /*
             * av_packet_free和av_packet_unref都是FFmpeg库中的函数,用于释放AVPacket结构体中的资源。
             * av_packet_free函数会释放AVPacket结构体中的所有资源,包括存储数据的缓冲区等。
             * av_packet_unref函数只会释放AVPacket结构体中的部分资源,例如释放存储数据的缓冲区等,但不会释放AVPacket结构体本身。
            */
            av_packet_unref(packet);
        }
        else
        {
            qDebug()<<"视频播放到结尾,播放结束";
            is_finish = true;
            //break;
        }
    }

}

VideoPlayer类(视频显示)

videoplayer.h

#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QWidget>
#include <QThread>
#include <QMutex>
#include "ffmpegvideodecode.h"

namespace Ui {
class VideoPlayer;
}

class VideoPlayer : public QWidget
{
    Q_OBJECT

public:
    explicit VideoPlayer(QWidget *parent = 0);
    ~VideoPlayer();

signals:
    void sigToStart(QString name);
    void sigToStop();
    void sigToFinish();

public slots:
    void onUpdateImage(const QImage &image);

protected:
    void paintEvent(QPaintEvent *event) override;

private:
    Ui::VideoPlayer *ui;

    FFmpegVideoDecode *ffmpeg_decode;
    QThread *thread_decode;


    QPixmap pixmap_video;
    QMutex m_mutex;
};

#endif // VIDEOPLAYER_H

videoplayer.cpp

#include "videoplayer.h"
#include "ui_videoplayer.h"
#include <QPainter>

VideoPlayer::VideoPlayer(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::VideoPlayer)
{
    ui->setupUi(this);

    ffmpeg_decode = new FFmpegVideoDecode;
    thread_decode = new QThread;
    ffmpeg_decode->moveToThread(thread_decode);
    connect(thread_decode,&QThread::finished,ffmpeg_decode,&FFmpegVideoDecode::deleteLater);
    connect(thread_decode,&QThread::finished,thread_decode,&QThread::deleteLater);
    connect(ffmpeg_decode,&FFmpegVideoDecode::sigToUpdateImage,this,&VideoPlayer::onUpdateImage);
    connect(this,&VideoPlayer::sigToStart,ffmpeg_decode,&FFmpegVideoDecode::onStartPlay);
    connect(this,&VideoPlayer::sigToFinish,ffmpeg_decode,&FFmpegVideoDecode::onFinish,Qt::DirectConnection);
    //方法1:由于子线程阻塞式延时读取视频帧,无法修改视频的暂停状态,可通过修改connect的连接方式在主线程中修改暂停状态
    connect(this,&VideoPlayer::sigToStop,ffmpeg_decode,&FFmpegVideoDecode::onStopPlay,Qt::DirectConnection);
    //方法2:将子线程阻塞式延时修改未非阻塞式,可直接修改视频的暂停状态
//    connect(this,&VideoPlayer::sigToStop,ffmpeg_decode,&FFmpegVideoDecode::onStopPlay);

    thread_decode->start();
}

VideoPlayer::~VideoPlayer()
{
    delete ui;
    Q_EMIT sigToFinish();
    thread_decode->quit();
    thread_decode->wait();
}

void VideoPlayer::onUpdateImage(const QImage &image)
{
    m_mutex.lock();
    pixmap_video = QPixmap::fromImage(image);
    m_mutex.unlock();
    update();
}

void VideoPlayer::paintEvent(QPaintEvent *event)
{
    if(!pixmap_video.isNull())
    {
        QPainter painter(this);
        m_mutex.lock();
        QPixmap pixmap = pixmap_video.scaled(this->size(), Qt::KeepAspectRatio);
        m_mutex.unlock();
        int x = (this->width() - pixmap.width()) / 2;
        int y = (this->height() - pixmap.height()) / 2;
        painter.drawPixmap(x, y, pixmap);
    }
    QWidget::paintEvent(event);
}

Widget类(主界面)

widget.h

#include "videoplayer.h"
#include "ui_videoplayer.h"
#include <QPainter>

VideoPlayer::VideoPlayer(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::VideoPlayer)
{
    ui->setupUi(this);

    ffmpeg_decode = new FFmpegVideoDecode;
    thread_decode = new QThread;
    ffmpeg_decode->moveToThread(thread_decode);
    connect(thread_decode,&QThread::finished,ffmpeg_decode,&FFmpegVideoDecode::deleteLater);
    connect(thread_decode,&QThread::finished,thread_decode,&QThread::deleteLater);
    connect(ffmpeg_decode,&FFmpegVideoDecode::sigToUpdateImage,this,&VideoPlayer::onUpdateImage);
    connect(this,&VideoPlayer::sigToStart,ffmpeg_decode,&FFmpegVideoDecode::onStartPlay);
    connect(this,&VideoPlayer::sigToFinish,ffmpeg_decode,&FFmpegVideoDecode::onFinish,Qt::DirectConnection);
    //方法1:由于子线程阻塞式延时读取视频帧,无法修改视频的暂停状态,可通过修改connect的连接方式在主线程中修改暂停状态
    connect(this,&VideoPlayer::sigToStop,ffmpeg_decode,&FFmpegVideoDecode::onStopPlay,Qt::DirectConnection);
    //方法2:将子线程阻塞式延时修改未非阻塞式,可直接修改视频的暂停状态
//    connect(this,&VideoPlayer::sigToStop,ffmpeg_decode,&FFmpegVideoDecode::onStopPlay);

    thread_decode->start();
}

VideoPlayer::~VideoPlayer()
{
    delete ui;
    Q_EMIT sigToFinish();
    thread_decode->quit();
    thread_decode->wait();
}

void VideoPlayer::onUpdateImage(const QImage &image)
{
    m_mutex.lock();
    pixmap_video = QPixmap::fromImage(image);
    m_mutex.unlock();
    update();
}

void VideoPlayer::paintEvent(QPaintEvent *event)
{
    if(!pixmap_video.isNull())
    {
        QPainter painter(this);
        m_mutex.lock();
        QPixmap pixmap = pixmap_video.scaled(this->size(), Qt::KeepAspectRatio);
        m_mutex.unlock();
        int x = (this->width() - pixmap.width()) / 2;
        int y = (this->height() - pixmap.height()) / 2;
        painter.drawPixmap(x, y, pixmap);
    }
    QWidget::paintEvent(event);
}

widget.cpp

#include "widget.h"
#include "ui_widget.h"

#include <QFileDialog>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    player = new VideoPlayer(ui->widget_play);
    ui->horizontalLayout->addWidget(player);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_pushButton_start_clicked()
{
    Q_EMIT player->sigToFinish();
    Q_EMIT player->sigToStart(ui->lineEdit_path->text());
    ui->pushButton_stop->setText("暂停");
}

void Widget::on_pushButton_stop_clicked()
{
    Q_EMIT player->sigToStop();
    if(ui->pushButton_stop->text() == "暂停")
        ui->pushButton_stop->setText("恢复");
    else if(ui->pushButton_stop->text() == "恢复")
        ui->pushButton_stop->setText("暂停");
}

void Widget::on_pushButton_select_clicked()
{
    QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
                                                     "/home",
                                                     tr("Video (*.mp4 *.flv)"));
    if(fileName.isEmpty()) return;
    ui->lineEdit_path->setText(fileName);
    qDebug()<<"选择文件:"<<fileName;
}

widget.ui

三、运行

1.显示本地视频

直接选择本地视频文件即可,显示界面如下:

2.显示拉流视频

启动mediamtx流媒体服务器(相关内容看上一篇笔记)

./mediamtx

循环推送本地视频到mediamtx服务器

ffmpeg -re -stream_loop -1 -i /home/li/1.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream

即可通过流地址显示视频,界面显示如下:

 代码下载地址:

https://download.csdn.net/download/m0_67254672/89118955

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/543123.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL——多表连接查询

若一个查询同时涉及两个或两个以上的表&#xff0c; 则称之为连接查询&#xff08;在FROM子句中体现)。 参与连接的表可有多个&#xff0c;但连接操作在两个表之间进行&#xff0c;即两两连接。 连接查询包括&#xff1a; 内连接 等值连接&#xff1a;用“”比较被连接列的列值…

Mistral AI突围:开源大模型Mixtral 8x22B颠覆行业格局

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

SVN的介绍

首先SVN是什么&#xff1a; Apache下的一个开源的项目Subversion&#xff0c;通常缩写为 SVN&#xff0c;是一个版本控制系统。 版本控制系统是一个软件&#xff0c;它可以伴随我们软件开发人员一起工作&#xff0c;让我们编写代码的完整的历史保存下来。 目前它的各个版本的…

RTC的基本概念以及相关例程

实时时钟(RTC) 北京时间跟伦敦时间错8个小时 BKP简介 BKP本质上是RAM存储器&#xff0c;没有掉电不丢失的能力。 VBAT的作用就是&#xff0c;当VDD断电时&#xff0c;BKP会切换到VBAT供电&#xff0c;这样可以继续维持BKP里面的数据&#xff0c;如果VDD断电&#xff0c;VBAT也…

YOLO系列 | 正负样本分配策略

文章目录 1 Max-IoU matching(YOLOv1~V3)2 Multi-Anchor策略(YOLOv4)3 基于宽高比的领域匹配策略(YOLOv5)4 simOTA(Simple Optimal Transport Assignment)匹配策略(YOLOX, YOLOv6)5 领域匹配simOTA(YOLOv7)6 TaskAlignedAssigner匹配策略(YOLOv8, YOLOv9)参考资料 1 Max-IoU ma…

Appium的使用:混合APP切换上下文

网上别的文章说要把移动端的webview设置成调试模式,才能看到下图信息。 但我这里是直接在Android Studio新建了一个空白活动,然后放的webview控件,写的webview代码,直接部署到模拟器上,在确定adb可以连接到模拟器后,在桌面浏览器输入chrome://inspect/#devices后就可以看…

代码随想录训练营

Day23代码随想录 669.修剪二叉搜索树 1.题目描述 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即&#xff0c;如果没有…

最坏情况为线性时间的第k大元素

在统计和数据分析中&#xff0c;我们经常会遇到求最大值、最小值、中位数、四分位数、Top K等类似需求&#xff0c;其实它们都属于顺序统计量&#xff0c;本文将对顺序统计量的定义和求解算法进行介绍&#xff0c;重点介绍如何在最差时间复杂度也是线性的情况下求解第k大元素。…

Java 二叉数(1)

一、认识树 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。它具有以下的特点&#xff1a; 有一个特殊的…

ES6 promise

Promise是在 JavaScript&#xff08;也称为 ECMAScript-6&#xff09;中实现异步编程的一种方式。JavaScript 中使用Promise进行异步编程。对于异步编程&#xff0c;JavaScript 使用回调&#xff0c;但使用回调会出现一个问题&#xff0c;即回调地狱&#xff08;多个或依赖回调…

Kubernetes 升级不弃 Docker:KubeKey 的丝滑之道

作者&#xff1a;尹珉&#xff0c;KubeSphere Ambaasador&Contributor&#xff0c;KubeSphere 社区用户委员会杭州站站长。 引言 随着 Kubernetes 社区的不断发展&#xff0c;即将迎来 Kubernetes 1.30 版本的迭代。在早先的 1.24 版本中&#xff0c;社区作出一个重要决策…

前后端分离的Java医院云HIS信息管理系统源码(LIS源码+电子病历源码)

HIS系统采用主流成熟技术开发&#xff0c;软件结构简洁、代码规范易阅读&#xff0c;SaaS应用&#xff0c;全浏览器访问前后端分离&#xff0c;多服务协同&#xff0c;服务可拆分&#xff0c;功能易扩展。多医院、多集团统一登录患者主索引建立、主数据管理&#xff0c;统一对外…

李廉洋;4.11#黄金,WTI原油#行情走势分析策略。

美国银行预计&#xff0c;在今天召开的欧洲央行会议上不会有重大的政策变化&#xff0c;但欧洲央行正逐渐接近开始降息&#xff0c;尽管它采取的是一种谨慎的、依赖数据的方式。虽然欧洲央行对降息轨迹的信心不断增强&#xff0c;但降息的具体速度和幅度仍未公布&#xff0c;而…

【深度学习实战(2)】如何使用matplotlib.pyplot模块记录自己的训练,验证损失

一、matplotlib库 在我们自己训练模型时&#xff0c;常常会使用matplotlib库来绘制oss和accuracy的曲线图&#xff0c;帮助我们分析模型的训练表现。 matplotlib库安装&#xff1a;pip install matplotlib 二、代码 import matplotlib.pyplot as plt import torch import to…

CloudCompare——体元法计算树冠体积

目录 1.概述2.软件实现3.完整操作4.相关代码 本文由CSDN点云侠原创&#xff0c;CloudCompare——体元法计算树冠体积&#xff0c;爬虫自重。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫与GPT生成的文章。 1.概述 体元法将树冠所在的空间范围划…

直驱式风电机组与双馈风电机组的发电机之间显著的区别

直驱式风电机组与双馈风电机组的发电机之间存在一些显著的区别&#xff0c;这些区别主要体现在以下几个方面&#xff1a; 结构设计&#xff1a;直驱式风力发电机组的发电机通常采用多极结构&#xff0c;包括多极内转子结构和多极外转子结构等&#xff0c;结构相对简单。而双馈…

NVM下载和安装NodeJS教程

nvm文档手册 - nvm是一个nodejs版本管理工具 - nvm中文网 官网下载nvm&#xff0c;选择版本 打开nvm文件夹下的settings.txt文件&#xff0c;在最后添加以下代码&#xff1a; node_mirror: https://npm.taobao.org/mirrors/node/ npm_mirror: https://npm.taobao.org/mirrors…

NAT技术

网络技术深似海呀&#xff0c;一段时间不用又忘。 是什么 NAT技术是网络防火墙技术的一部分&#xff0c;可以作用在linux防火墙或者设备防火墙&#xff0c;NAT技术可以实现地址和端口的转换&#xff0c;主要还是为了网络连通性。 作用 存在以下三个IP&#xff0c;A(10.234.…

vue3+element plus图片预览点击按钮直接显示图片的预览形式

1 需求 直接上需求&#xff1a; 我想要直接点击下面这个“预览”按钮&#xff0c;然后呈现出预览图片的形式 ok&#xff0c;需求知道了&#xff0c;下面让我们来看看如何实现吧 ~ 2 实现 template部分 <el-buttontype"primary"size"small"click&qu…

Java集合(一)--Map(2)

ConcurrentHashMap与HashTable 底层实现 在JDK1.7时&#xff0c;底层采用的是分段数组&#xff0b;链表的形式&#xff0c;在JDK1.8之后&#xff0c;采用的是与HashMap相同的形式&#xff0c;数组链表/红黑树。而HashTable采用的是数组链表的形式。 如何实现线程安全 Concu…