• QT软件开发-基于FFMPEG设计视频播放器-支持软解与硬解(一)


    QT软件开发-基于FFMPEG设计视频播放器-CPU软解视频(一)
    https://xiaolong.blog.csdn.net/article/details/126832537

    QT软件开发-基于FFMPEG设计视频播放器-GPU硬解视频(二)
    https://xiaolong.blog.csdn.net/article/details/126833434

    QT软件开发-基于FFMPEG设计视频播放器-解码音频(三)
    https://xiaolong.blog.csdn.net/article/details/126836582

    QT软件开发-基于FFMPEG设计视频播放器-OpenGL渲染视频(四)
    https://xiaolong.blog.csdn.net/article/details/126842988

    QT软件开发-基于FFMPEG设计视频播放器-流媒体播放器(五)
    https://xiaolong.blog.csdn.net/article/details/126915003

    QT软件开发-基于FFMPEG设计视频播放器-视频播放器(六)
    https://blog.csdn.net/xiaolong1126626497/article/details/126916817

    一、前言

    说起ffmpeg,只要是搞音视频相关的开发应该都是听过的。FFmpeg提供了非常先进的音频/视频编解码库,并且支持跨平台。

    现在互联网上ffmpeg相关的文章、教程也非常的多,ffmpeg本身主要是用来对视频、音频进行解码、编码。本身不提供图片渲染、声音输出等功能。要设计出自己的一款播放器,首先也得需要其他的一些知识配合调用ffmpeg才能完成任务。

    为了能简单快速的介绍ffmpeg使用,我这里的会连续通过几篇文章,编写几个案例来循序渐进的演示ffmpeg的用法,最终完成一个完整的播放器开发,不涉及理论知识(理论知识网上太多了),主要是以代码、以实现功能为主。

    我这里开发视频播放器用到的环境介绍:

    ffmpeg版本:  4.2.2
    Qt版本    :  5.12.6
    编译器类型 : MinGW32bit 
    

    这几篇文章循序渐进编写的内容规划与案例如下:

    (1)利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

    特点: 采用软件解码(CPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

    (2)利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,解码后从GPU里拷贝数据到CPU,完成像素转换,再通过QWidget渲染图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

    特点: 采用硬件加速解码(GPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

    (3)利用ffmpeg解码视频里的音频包,通过QAudioOutput播放音频数据。

    特点: 只解码音频数据,忽略视频图像数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成音频数据解码,转换音频数据格式,最后通过QAudioOutput播放出来。

    (4)利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

    特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程。

    (5)在第(4)个例子上增加流媒体播放支持,支持rtmp、rtsp、HLS(HTTP协议)等常见的流媒体格式支持,利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据。

    特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程。

    (6)结合第(3)个例子和第(5)例子,增加音频包解码播放,利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据,通过QAudioOutput播放音频数据。支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

    特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程,通过QAudioOutput播放音频数据的流程。

    (7)结合前面的例子合并,设计完成的视频播放器。

    二、解码与渲染

    如果要做一个视频播放器,主要解决3个问题:(1)解码 (2)渲染 (3)音视频同步

    2.1 解码

    ffmpeg支持纯软件解码和硬件加速解码。 纯软件解码只要是依靠CPU,如果分辨率较大(4K及以上)的视频软解会占用很高的CPU,并且解码速度也比较慢,加上渲染的时间,整体视频播放器就有卡顿现象。 如果利用硬件加速(GPU)解码,解码的时间会大大缩短。我的电脑是i7低功耗CPU,在我电脑上测试:一个分辨率为3840x2160的视频,软解一帧耗时300ms左右,如果启用硬件加速解码,一帧耗时10ms左右,可以速度相差是非常大的。 并且软解时,CPU占用几乎是100%,如果通过GPU解码,CPU的负荷就降的非常低,可以腾出更多时间去干别的事情。

    软解和硬解本身差别不是很大,因为ffmpeg已经将API全部封装好了,只需要调用即可,不需要去了解底层的很多东西,开发起来非常方便。

    ffmpeg源码下提供了很多例子,其中就有视频解码的例子。

    ffmpeg\doc\examples\decode_video.c
    ffmpeg\doc\examples\hw_decode.c
    

    这两个例子都很有参考价值,其中 hw_decode.c 就是ffmpeg硬件加速解码的例子,通过这个例子就可以理解ffmpeg如何调用GPU进行硬件解码。

    当然,ffmpeg也带了一个命令行的播放器,源码就是ffplay.c,这个代码实现的很完善,就是一个播放器,只不过ffplay.c的代码比较多,除了ffmpeg本身的API调用以外,渲染的部分是通过SDL实现的,如果前期对ffmpeg、SDL不怎么熟悉,直接去看ffplay.c可能效果不是太好。

    那最好的办法就是先从简单开始,循序渐进的理解,最后再去看ffplay.c,这样效果就会好很多。

    2.2 渲染

    ffmpeg本身只是解码、编码的库,解码出来的图像渲染要自己实现。所谓的渲染就是显示ffmpeg解码视频之后得到的图片数据。 渲染也分为软件渲染、硬件加速渲染。 当前我这里的UI是采用Qt做的,在Qt里显示图片的方式很多,可以直接通过Qwidget绘制、QLabel显示等。这种方式是最常规的方式,也是最简单的方式,这种方式绘制就是采用CPU,对CPU占用较高,而且要通过Qwidget、QLabel等方式显示,需要将ffmpeg解码出来的数据转换像素格式,再封装为QImage格式,这个过程是很费时间的。如果要降低CPU占用,加快渲染速度,可以采用OpenGL渲染,Qt里封装了QOpenGLWidget,调用OpenGL也相对很方便。

    三、视频播放器设计

    3.1 设计说明

    利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

    特点: 采用软件解码(CPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

    视频解码采用独立子线程,解码后将得到的图像数据通过信号槽发方式传递给UI界面进行渲染。

    3.2 解码流程

    下面是ffmpeg软件解码的基本流程:

    //1.打开多媒体流,并且获取一些信息
    avformat_open_input(&format_ctx, m_MediaFile, nullptr, nullptr)
    
    
    //2. 读取媒体文件的数据包以获取流信息
    avformat_find_stream_info(format_ctx, nullptr)
    
    
    //3. 查找解码器
    AVCodec *video_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);
    
    //4. 打开解码器
    avcodec_open2(stream->codec,video_pCodec,nullptr)
    
    //5. 读取一帧数据
    av_read_frame(format_ctx, &pkt)
    
    //6. 发送视频帧
    avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt)
    
    //7. 对视频帧进行解码
    avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame)
    
    //8. 转换像素格式
    sws_scale(img_convert_ctx,
    	   (uint8_t const **) SRC_VIDEO_pFrame->data,
    	   SRC_VIDEO_pFrame->linesize, 0, video_height, RGB24_pFrame->data,
    	   RGB24_pFrame->linesize);
    
    //9. 渲染
    

    这里面耗时较多的是sws_scale,视频分辨率越大,消耗的时间越长。其次就是avcodec_receive_frame,和渲染,如果软解的视频超过4K,解码就特别消耗时间,如果低于4K分辨率,解码消耗的时间还能接受的。

    3.3 运行效果

    image-20220913140928520

    四、源代码

    4.1 解码线程.cpp

    //指定文件的编码为UTF-8
    #pragma execution_character_set("utf-8")
    
    #include "ReverseDecodThread.h"
    
    ReverseDecodThread::ReverseDecodThread()
    {
        qDebug() << "FFMPEG版本信息:" << av_version_info();
    }
    
    ReverseDecodThread::~ReverseDecodThread()
    {
    
    }
    
    
    /*
    功能: 设置媒体文件
    */
    int ReverseDecodThread::set_VideoFile(QString media)
    {
        //打开媒体文件
        QByteArray array=media.toUtf8();
        strncpy(m_MediaFile, array.data(), sizeof(m_MediaFile));
    }
    
    
    void ReverseDecodThread::SetSate(int run)
    {
    	m_run = run;
    }
    
    int ReverseDecodThread::GetSate()
    {
    	return m_run;
    }
    
    
    //跳转视频帧
    void ReverseDecodThread::SetSeekPos(qint64 pos)
    {
    	is_CurrentSeekPos = 1;
        m_n64CurrentSeekPos = pos;
        m_run=1;  //运行状态
    
        //获取系统本地时间
        play_base_time=QDateTime::currentMSecsSinceEpoch();
    }
    
    
    void ReverseDecodThread::PausePlay()
    {
    	m_run = 2;
    }
    
    void ReverseDecodThread::StopPlay()
    {
    	m_run = 0;
    }
    
    void ReverseDecodThread::LogSend(QString text)
    {
    	qDebug() << text;
    }
    
    
    //线程执行起点
    void ReverseDecodThread::run()
    {
        LogSend("开始播放视频.\n");
        StartPlay();
    }
    
    
    //播放视频
    int ReverseDecodThread::StartPlay()
    {
        //1.打开多媒体流,并且获取一些信息
        if(avformat_open_input(&format_ctx, m_MediaFile, nullptr, nullptr) != 0)
        {
             LogSend(tr("无法打开视频文件: %1").arg(m_MediaFile));
             return -1;
        }
    
        //2. 读取媒体文件的数据包以获取流信息
        if(avformat_find_stream_info(format_ctx, nullptr) < 0)
        {
            LogSend(tr("无法获取流信息.\n"));
            return -1;
        }
    
        //3.打印视频的信息
        LogSend(tr("视频中流的数量: %1\n").arg(format_ctx->nb_streams));
        for(int i = 0; i < format_ctx->nb_streams; ++i)
        {
            const AVStream* stream = format_ctx->streams[i];
            if(stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
            {
                //查找解码器
                AVCodec *video_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);
                //打开解码器
                if(avcodec_open2(stream->codec,video_pCodec,nullptr)!=0)
                {
                      LogSend(tr("解码器打开失败.\n"));
                      return -1;
                }
                video_stream_index = i;
                //得到视频帧的宽高
                video_width=stream->codecpar->width;
                video_height=stream->codecpar->height;
    
                LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg(
                    stream->codecpar->width).arg(stream->codecpar->height).arg(stream->codecpar->format));
            }
        }
    
        if (video_stream_index == -1)
        {
             LogSend("没有检测到视频流.\n");
             return -1;
        }
    
        AVRational frameRate = format_ctx->streams[video_stream_index]->avg_frame_rate;
    
        /*设置视频转码器*/
        SRC_VIDEO_pFrame = av_frame_alloc();
        RGB24_pFrame = av_frame_alloc();// 存放解码后YUV数据的缓冲区
    
        //将解码后的YUV数据转换成RGB24
        img_convert_ctx = sws_getContext(video_width, video_height,
                format_ctx->streams[video_stream_index]->codec->pix_fmt,video_width, video_height,
                AV_PIX_FMT_RGB24, SWS_BICUBIC, nullptr, nullptr, nullptr);
    
        //计算RGB图像所占字节大小
        int numBytes=avpicture_get_size(AV_PIX_FMT_RGB24,video_width,video_height);
    
        //申请空间存放RGB图像数据
        out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    
        // avpicture_fill函数将ptr指向的数据填充到picture内,但并没有拷贝,只是将picture结构内的data指针指向了ptr的数据
        avpicture_fill((AVPicture *) RGB24_pFrame, out_buffer_rgb, AV_PIX_FMT_RGB24,
                video_width, video_height);
    
        qDebug()<<"format_ctx->duration:"<<format_ctx->duration;
    
        //获取系统本地时间
        play_base_time=QDateTime::currentMSecsSinceEpoch();
    
        //表示视频加载成功
        while(m_run)
        {
            if(m_run == 2)
            {
                msleep(100); //暂停播放
    			continue;
            }
    
    		if (is_CurrentSeekPos)
    		{
    			is_CurrentSeekPos = 0;
                //偏移到指定位置再开始解码    AVSEEK_FLAG_BACKWARD 向后找最近的关键帧
                av_seek_frame(format_ctx, -1, m_n64CurrentSeekPos* AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);
                qDebug()<<"跳转的位置:"<<m_n64CurrentSeekPos;
    		}
    
            double video_clock;
            AVPacket pkt;
    
            //1. 读取一帧数据
            if(av_read_frame(format_ctx, &pkt) < 0)
            {
                m_run=2; //设置为暂停状态
                qDebug()<<"数据读取完毕.";
                continue;
            }
    
            if(pkt.stream_index == video_stream_index)
            {
                //当前时间
               video_clock = av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts;
    
               qDebug()<<"pkt.pts:"<<pkt.pts<<"video_clock:"<<video_clock;
    
    
               //解码视频 frame
               //2. 发送视频帧
                if (avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt) != 0)
                {
                    av_packet_unref(&pkt);//不成功就释放这个pkt
                    continue;
                }
    
                //3. 接受后对视频帧进行解码
                if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) != 0)
                {
                    av_packet_unref(&pkt);//不成功就释放这个pkt
                    continue;
                }
    
                //4. 转格式
               sws_scale(img_convert_ctx,
                       (uint8_t const **) SRC_VIDEO_pFrame->data,
                       SRC_VIDEO_pFrame->linesize, 0, video_height, RGB24_pFrame->data,
                       RGB24_pFrame->linesize);
    
    
        
               //5. 加载图片数据
               QImage image(out_buffer_rgb,video_width,video_height,QImage::Format_RGB888);
    
    
               //通过pts同步视频帧--显示视频帧
    //           while (true)
    //           {
    //                qint64 t1=QDateTime::currentMSecsSinceEpoch();
    //                qint64 t2=t1-play_base_time;
    
    //                qDebug()<<"t1:"<
    //                qDebug()<<"t2:"<
    //                qDebug()<<"video_clock:"<
    
    //                if(t2>=video_clock*1000)
    //                {
    //                    break;
    //                }
    //                else
    //                {
    //                     QThread::msleep(1);
    //                }
    //           }
    
               //通知界面更新
               VideoDataOutput(image.copy());
    
               //时间信号
               sig_getCurrentTime(video_clock, format_ctx->duration *1.0 / AV_TIME_BASE);
    
              // QThread::msleep(40);
            }
                   //释放包
               av_packet_unref(&pkt);
    
        }
    
        LogSend("视频音频解码播放器的线程退出成功.\n");
    
        if(SRC_VIDEO_pFrame) av_frame_free(&SRC_VIDEO_pFrame);
        if(RGB24_pFrame) av_frame_free(&RGB24_pFrame);
        if(img_convert_ctx)sws_freeContext(img_convert_ctx);
        if(out_buffer_rgb)av_free(out_buffer_rgb);
    
        SRC_VIDEO_pFrame=nullptr;
        RGB24_pFrame=nullptr;
        img_convert_ctx=nullptr;
        out_buffer_rgb=nullptr;
    
        if(format_ctx)
        {
            avformat_close_input(&format_ctx);//释放解封装器的空间,以防空间被快速消耗完
            avformat_free_context(format_ctx);
        }
    
        return 0;
    }
    

    4.2 解码线程.h

    #ifndef VIDEO_PLAY_H
    #define VIDEO_PLAY_H
    
    #include 
    #include 
    #include 
    #include 
    
    extern "C" {
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    }
    
    
    //视频音频解码线程
    class ReverseDecodThread: public QThread
    {
        Q_OBJECT
    public:
    
        //构造函数
        ReverseDecodThread();
        ~ReverseDecodThread();
    	char m_MediaFile[1024];
    	int m_run; //1表示运行 0表示停止 2表示暂停
        double m_n64CurrentSeekPos = 0;  //当前seek位置
    	bool is_CurrentSeekPos = 0; //1需要跳转 0不需要
    
    	void SetSate(int run);
    	int GetSate();
    	void SetSeekPos(qint64 pos);
    	void PausePlay();
    	void StopPlay();
    	void LogSend(QString text);
    
        //加载视频文件
        int set_VideoFile(QString media);
    
    protected:
        void run();
    	int StartPlay();
    signals:
        void sig_getCurrentTime(double Sec, double total_Sec);
        void VideoDataOutput(QImage); //输出信号
    private:
        int video_width=0;
        int video_height=0;
        AVFormatContext *format_ctx=nullptr;
        int video_stream_index = -1;
        AVFrame *RGB24_pFrame = nullptr;
        AVFrame *SRC_VIDEO_pFrame= nullptr;
        uint8_t *out_buffer_rgb= nullptr;
        struct SwsContext *img_convert_ctx=nullptr;  //用于解码后的视频格式转换
    
        double video_clock_tmp;
    
        qint64 play_base_time=0;
    };
    #endif // VIDEO_PLAY_H
    

    4.3 widget渲染

    #include "VideoFrameDisplay.h"
    
    #include 
    
    VideoFrameDisplay::VideoFrameDisplay(QWidget *parent) :
        QWidget(parent)
    {
        m_nRotateDegree=0;
        this->setMouseTracking(true);
    }
    
    VideoFrameDisplay::~VideoFrameDisplay()
    {
    
    }
    
    void VideoFrameDisplay::Set_Rotate(int Rotate)
    {
        m_nRotateDegree=Rotate;
    }
    
    void VideoFrameDisplay::paintEvent(QPaintEvent *event)
    {
        QPainter painter(this);
    	
    	painter.setRenderHint(QPainter::Antialiasing);
        painter.setRenderHint(QPainter::TextAntialiasing);
        painter.setRenderHint(QPainter::SmoothPixmapTransform);
        painter.setRenderHint(QPainter::HighQualityAntialiasing);
    	
        painter.setBrush(Qt::black);
        painter.drawRect(0,0,this->width(),this->height()); //先画成黑色
    
        if (mImage.size().width() <= 0) return;
    
        //将图像按比例缩放成和窗口一样大小
        QImage img = mImage.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 
    
        //画面旋转
        if(m_nRotateDegree > 0)
        {
            QTransform matrix;
            matrix.rotate(m_nRotateDegree);
            img = img.transformed(matrix, Qt::SmoothTransformation);
        }
    
        int x = this->width() - img.width();
        int y = this->height() - img.height();
    
        x /= 2;
        y /= 2;
    
        painter.drawImage(QPoint(x,y),img); //画出图像
    }
    
    
    void VideoFrameDisplay::slotSetOneFrame(QImage img)
    {
        src_mImage =mImage = img;
        update(); //调用update将执行 paintEvent函数
    }
    
    
    /*
    功能: 获取原图数据
    */
    QImage VideoFrameDisplay::GetImage()
    {
        return src_mImage.copy();
    }
    
    /*
    功能: 鼠标双击事件
    */
    void VideoFrameDisplay::mouseDoubleClickEvent(QMouseEvent *e)
    {
        emit s_VideoWidgetEvent(1);
    }
    

    4.4 ui主线程widget.cpp

    #include "widget.h"
    #include "ui_widget.h"
    
    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        ui->horizontalSlider_time->installEventFilter(this);
    
        //关联视频解码器
        connect(&DecodeWorkThread, SIGNAL(VideoDataOutput(QImage)), ui->widget_video, SLOT(slotSetOneFrame(QImage)));
    
        //当前时间
        connect(&DecodeWorkThread, SIGNAL(sig_getCurrentTime(double, double)), this, SLOT(slotGetCurrentTime(double, double)));
    }
    
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    
    void Widget::slotGetCurrentTime(double pts, double duration)
    {
        ui->horizontalSlider_time->setMaximum(duration);
        ui->horizontalSlider_time->setMinimum(0);
        ui->horizontalSlider_time->setValue(pts);
    
        ui->label_duration->setText(QString("%1/%2").arg(pts).arg(duration));
    }
    
    
    void Widget::on_pushButton_play_clicked()
    {
        DecodeWorkThread.SetSate(0);
        DecodeWorkThread.quit();
        DecodeWorkThread.wait();
    
        DecodeWorkThread.SetSate(1);
    
        QString filename = QFileDialog::getOpenFileName(this, "选择打开的文件", "C:/", tr("*.*"));
        DecodeWorkThread.set_VideoFile(filename);
        DecodeWorkThread.start();
    }
    
    
    void Widget::on_pushButton_pause_clicked()
    {
        if (DecodeWorkThread.GetSate() == 2)
        {
            DecodeWorkThread.SetSate(1);
        }
        else if(DecodeWorkThread.GetSate() == 1)
        {
            DecodeWorkThread.SetSate(2);
        }
    }
    
    
    void Widget::on_pushButton_stop_clicked()
    {
         DecodeWorkThread.SetSate(0);
    }
    
    
    bool Widget::eventFilter(QObject *obj, QEvent *event)
    {
        //解决QSlider点击不能到鼠标指定位置的问题
        if(obj==ui->horizontalSlider_time)
        {
            if (event->type()==QEvent::MouseButtonPress) //判断类型
            {
                QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
                if (mouseEvent->button() == Qt::LeftButton)	//判断左键
                {
                   int value = QStyle::sliderValueFromPosition(ui->horizontalSlider_time->minimum(), ui->horizontalSlider_time->maximum(), mouseEvent->pos().x(), ui->horizontalSlider_time->width());
                   ui->horizontalSlider_time->setValue(value);
    
                   qDebug()<<"value:"<<value;
                   //跳转到指定位置
                   DecodeWorkThread.SetSeekPos(ui->horizontalSlider_time->value());
                }
            }
        }
        return QObject::eventFilter(obj,event);
    }
    

    4.5 pro文件(加载ffmpeg库)

    win32
    {
        message('运行win32版本')
        INCLUDEPATH+=C:/FFMPEG/ffmpeg_x86_4.2.2/include
        LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/av*
        LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/sw*
        LIBS+=C:/FFMPEG/ffmpeg_x86_4.2.2/bin/pos*
    }
    
  • 相关阅读:
    javascript中的new原理及实现
    warning: usage of JAVA_HOME is deprecated, use ES_JAVA_HOME
    java的子类如何重写父类的方法?
    android-handler
    个人网页制作 个人网页设计作业 HTML CSS个人网页模板 大学生个人介绍网站毕业设计 DW个人主题网页模板下载 个人网页成品代码 个人网页作品下载
    Ranger配置图片及json文件预览
    kubeflow核心功能
    矩阵特征值与特征向量的理解
    【限定词习题】enough/too + 不定式
    百度通用文字识别离线SDK部署(c#)
  • 原文地址:https://blog.csdn.net/xiaolong1126626497/article/details/126832537