播放器开发(四):多线程解复用与解码模块实现

学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】

前言

根据第一章内容,我们首先可以先把解复用和解码模块完成,其中需要使用到多线程以及队列,还需要使用FFmpeg进行解复用和解码动作的实现。

创建BaseQueue基类

BaseQueue.h

#include <condition_variable>
#include <mutex>
#include <queue>using namespace std;template<class T>
class BaseQueue {
public:/*** 唤醒所有等待线程,设置异常标识为1*/void abort() {m_abort = 1;m_cond.notify_all();}/*** push进m_queue** @param val 需要push的值* @return  1是成功 or -1是m_abort==1可能有异常*/int push(T val) {lock_guard<mutex> lock(m_mutex);if (m_abort == 1) {return -1;}m_queue.push(val);m_cond.notify_one();return 0;}/*** 从基类 front 到 val 并执行基类std::queue.pop** @param val 存放front值的地址引用* @param timeout 持有锁的上限时间(ms)* @return 1是成功 or -1是m_abort==1可能有异常 or 是m_queue为空*/int pop(T &val, int timeout = 0) {unique_lock<mutex> lock(m_mutex);if (m_queue.empty()) {m_cond.wait_for(lock, chrono::microseconds(timeout), [this] {return !m_queue.empty() | m_abort;});}if (m_abort == 1) {return -1;}if (m_queue.empty()) {return -2;}// there is address referenceval = m_queue.front();m_queue.pop();return 0;}/*** 从基类std::queue.front 获取值保存到引用地址val** @param val 存放front值的地址引用* @return 1是成功 or -1是m_abort==1可能有异常 or 是m_queue为空*/int front(T &val) {lock_guard<mutex> lock(m_mutex);if (m_abort == 1) {return -1;}if (m_queue.empty()) {return -2;}val = m_queue.front();return 0;}int size() {lock_guard<mutex> lock(m_mutex);return m_queue.size();}private:int m_abort = 0;// 是否中止mutex m_mutex;  // 锁condition_variable m_cond;queue<T> m_queue;
};

创建AVPacketQueue包队列类和AVFrameQueue帧队列类

在AVPacketQueue和AVFrameQueue中分别实现模版BaseQueue基类,并且可以添加一些错误的信息打印。


我们可以新建一个头文件FFmpegHeader.h用来存放导入ffmpeg的一些代码,后面用的时候导入这个文件就可以了

FFmpegHeader.h

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavformat/avformat.h"
#include "libavutil/audio_fifo.h"
#include "libavutil/avassert.h"
#include "libavutil/ffversion.h"
#include "libavutil/frame.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavutil/pixdesc.h"
#include "libavutil/time.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"#ifdef ffmpegdevice
#include "libavdevice/avdevice.h"
#endif
}#include "qdatetime.h"
#pragma execution_character_set("utf-8")#define TIMEMS qPrintable(QTime::currentTime().toString("HH:mm:ss zzz"))
#define TIME qPrintable(QTime::currentTime().toString("HH:mm:ss"))
#define QDATE qPrintable(QDate::currentDate().toString("yyyy-MM-dd"))
#define QTIME qPrintable(QTime::currentTime().toString("HH-mm-ss"))
#define DATETIME qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"))
#define STRDATETIME qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss"))
#define STRDATETIMEMS qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss-zzz"))

AVPacketQueue

//AVPacketQueue.h
#include "BaseQueue.h"
#include "FFmpegHeader.h"
#include <cstdio>class AVPacketQueue {
public:~AVPacketQueue();int push(AVPacket *val);AVPacket *pop(int timeout);void release();int size();private:BaseQueue<AVPacket *> m_queue;
};//AVPacketQueue.cpp
#include "AVPacketQueue.h"AVPacketQueue::~AVPacketQueue() {release();m_queue.abort();
}int AVPacketQueue::push(AVPacket *val) {AVPacket *tmp_pkt = av_packet_alloc();av_packet_move_ref(tmp_pkt, val);return m_queue.push(tmp_pkt);
}
AVPacket *AVPacketQueue::pop(int timeout) {AVPacket *tmp_pkt = nullptr;int ret = m_queue.pop(tmp_pkt, timeout);if (ret == -1) {printf("AVPacketQueue::pop -->> m_abort==1可能有异常");}if (ret == -2) {printf("AVPacketQueue::pop -->> 队列为空");}return tmp_pkt;
}
void AVPacketQueue::release() {while (true) {AVPacket *pkt = nullptr;int ret = m_queue.pop(pkt, 1);if (ret < 0) {break;} else {av_packet_free(&pkt);continue;}}
}
int AVPacketQueue::size() {return m_queue.size();
}

AVFrameQueue

//AVFrameQueue.h
#include "BaseQueue.h"
#include "FFmpegHeader.h"
#include <cstdio>class AVFrameQueue {
public:~AVFrameQueue();int push(AVFrame *val);AVFrame *pop(int timeout);void release();int size();private:BaseQueue<AVFrame *> m_queue;
};//AVFrameQueue.cpp
#include "AVFrameQueue.h"
AVFrameQueue::~AVFrameQueue() {release();m_queue.abort();
}int AVFrameQueue::push(AVFrame *val) {AVFrame *tmp_frame = av_frame_alloc();av_frame_move_ref(tmp_frame, val);return m_queue.push(tmp_frame);
}
AVFrame *AVFrameQueue::pop(int timeout) {AVFrame *tmp_frame = nullptr;int ret = m_queue.pop(tmp_frame, timeout);if (ret == -1) {printf("AVFrameQueue::pop -->> m_abort==1可能有异常");}if (ret == -2) {printf("AVFrameQueue::pop -->> 队列为空");}return tmp_frame;
}
void AVFrameQueue::release() {while (true) {AVFrame *pkt = nullptr;int ret = m_queue.pop(pkt, 1);if (ret < 0) {break;} else {av_frame_free(&pkt);continue;}}
}
int AVFrameQueue::size() {return m_queue.size();
}

创建BaseThread抽象类

#include <QThread>/*** 因为我们后面可能需要用到qt信号传递是否暂停,所以使用QThread*/
class BaseThread : public QThread {Q_OBJECT
public:// 初始化virtual int init() = 0;// 创建线程 开始run工作virtual int start() = 0;// 停止线程 释放资源virtual void stop() {isStopped = true;if (m_thread) {if (m_thread->isRunning()) {m_thread->wait(-1);}delete m_thread;m_thread = nullptr;}};protected:bool isStopped = true; // 是否已经停止 停止时退出线程bool isPlaying = false;// 是否正在播放bool isPause = false;  // 是否暂停QThread *m_thread = nullptr;
};

创建DemuxThread解复用线程模块和DecodeThread解码线程模块

DemuxThread解复用线程:

1、打开视频文件,解封装操作。

2、读取流信息,并添加打印信息。

3、解复用(循环分离视频流和音频流)。

DemuxThread

//DemuxThread.h
enum class MediaType {Audio,Video
};class DemuxThread : public BaseThread {
private:QString m_url = nullptr;AVFormatContext *ic = nullptr;int m_videoStreamIndex = -1;int m_audioStreamIndex = -1;const AVCodec *m_videoCodec;const AVCodec *m_audioCodec;AVPacketQueue *m_audioQueue;AVPacketQueue *m_videoQueue;public:DemuxThread(AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue);DemuxThread(const QString &url, AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue);~DemuxThread() override;// 打开视频文件,读取信息int init() override;int start() override;void stop() override;void run() override;void setUrl(const QString &url);AVCodecParameters *getCodecParameters(MediaType type);AVRational *getStreamTimeBase(MediaType type);const AVCodec *getCodec(MediaType type);
};//DemuxThread.cpp
#include "DemuxThread.h"
DemuxThread::DemuxThread(AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue): m_audioQueue(mAudioQueue), m_videoQueue(mVideoQueue) {
}
DemuxThread::DemuxThread(const QString &url, AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue): m_url(url), m_audioQueue(mAudioQueue), m_videoQueue(mVideoQueue) {
}DemuxThread::~DemuxThread() {if (m_thread) {this->stop();}
}
int DemuxThread::init() {if (m_url == nullptr) {qDebug() << "没有设置文件链接";return -1;}ic = avformat_alloc_context();int ret;ret = avformat_open_input(&ic, m_url.toUtf8(), nullptr, nullptr);if (ret < 0) {qDebug() << "avformat_open_input 函数发送错误";return -1;}ret = avformat_find_stream_info(ic, nullptr);if (ret < 0) {qDebug() << "avformat_find_stream_info 函数发送错误";return -1;}m_videoStreamIndex = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &m_videoCodec, 0);if (m_videoStreamIndex < 0) {qDebug() << "没有找到视频流索引 av_find_best_stream error";return -1;}AVCodecParameters *codecParameters_video = ic->streams[m_videoStreamIndex]->codecpar;QString codecNameVideo = avcodec_get_name(codecParameters_video->codec_id);qDebug() << "视频流:" << codecNameVideo;m_audioStreamIndex = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, &m_audioCodec, 0);if (m_audioStreamIndex < 0) {qDebug() << "没有找到音频流索引 av_find_best_stream error";return -1;}AVCodecParameters *codecParameters_audio = ic->streams[m_audioStreamIndex]->codecpar;QString codecNameAudio = avcodec_get_name(codecParameters_audio->codec_id);qDebug() << "音频流:" << codecNameAudio;return 1;
}
int DemuxThread::start() {if (init() != 1) {qDebug() << "打开文件失败,停止创建线程";return -1;}isStopped = false;isPlaying = true;QThread::start();if (!currentThread()) {qDebug() << "线程创建失败";return -1;}return 0;
}
void DemuxThread::stop() {BaseThread::stop();if (ic) {avformat_close_input(&ic);ic = nullptr;}if (m_videoCodec) {m_videoCodec = nullptr;}if (m_audioCodec) {m_audioCodec = nullptr;}
}
void DemuxThread::run() {int ret;AVPacket pkt;while (!isStopped) {//if (m_audioQueue->size() > 10 || m_videoQueue->size() > 10) {//                        qDebug()<<"解复用线程等待 " <<"videoSize "<< m_videoQueue->size()<<"audioSize "<<m_audioQueue->size();msleep(10);//            std::this_thread::sleep_for(std::chrono::milliseconds(10));continue;}ret = av_read_frame(ic, &pkt);if (ret < 0) {qDebug() << "帧读完";break;}if (pkt.stream_index == m_audioStreamIndex) {m_audioQueue->push(&pkt);//            qDebug() << "audio pkt queue size:" << m_audioQueue->size();} else if (pkt.stream_index == m_videoStreamIndex) {m_videoQueue->push(&pkt);//            qDebug() << "video pkt queue size:" << m_videoQueue->size();} else {av_packet_unref(&pkt);}}
}void DemuxThread::setUrl(const QString &url) {m_url = url;
}
AVCodecParameters *DemuxThread::getCodecParameters(MediaType type) {switch (type) {case MediaType::Audio:if (m_audioStreamIndex != -1) {return ic->streams[m_audioStreamIndex]->codecpar;} else {return nullptr;}case MediaType::Video:if (m_videoStreamIndex != -1) {return ic->streams[m_videoStreamIndex]->codecpar;} else {return nullptr;}}
}
AVRational *DemuxThread::getStreamTimeBase(MediaType type) {switch (type) {case MediaType::Audio:if (m_audioStreamIndex != -1) {return &ic->streams[m_audioStreamIndex]->time_base;} else {return nullptr;}case MediaType::Video:if (m_videoStreamIndex != -1) {return &ic->streams[m_videoStreamIndex]->time_base;} else {return nullptr;}}
}
const AVCodec *DemuxThread::getCodec(MediaType type) {switch (type) {case MediaType::Audio:return m_audioCodec;case MediaType::Video:return m_videoCodec;}
}

DecodeThread

//DecodeThread.h
#include "BaseThread.h"
#include "FFmpegHeader.h"
#include "queue/AVFrameQueue.h"
#include "queue/AVPacketQueue.h"
#include <QDebug>class DecodeThread : public BaseThread {
private:const AVCodec *m_codec = nullptr;AVCodecParameters *m_par = nullptr;AVPacketQueue *m_packetQueue = nullptr;AVFrameQueue *m_frameQueue = nullptr;public:AVCodecContext *dec_ctx = nullptr;DecodeThread(const AVCodec *mCodec, AVCodecParameters *mPar, AVPacketQueue *mPacketQueue, AVFrameQueue *mFrameQueue);~DecodeThread() override;int init() override;int start() override;void stop() override;void run() override;
};//DecodeThread.cpp
#include "DecodeThread.h"
DecodeThread::DecodeThread(const AVCodec *mCodec, AVCodecParameters *mPar, AVPacketQueue *mPacketQueue, AVFrameQueue *mFrameQueue): m_codec(mCodec), m_par(mPar), m_packetQueue(mPacketQueue), m_frameQueue(mFrameQueue) {
}
DecodeThread::~DecodeThread() {stop();
}
int DecodeThread::init() {if (!m_par) {qDebug() << "AVCodecParameters 为空";return -1;}dec_ctx = avcodec_alloc_context3(nullptr);int ret = avcodec_parameters_to_context(dec_ctx, m_par);if (ret < 0) {qDebug() << "avcodec_parameters_to_context error";}ret = avcodec_open2(dec_ctx, m_codec, nullptr);if (ret < 0) {qDebug() << "avcodec_open2 error";}return 0;
}
void DecodeThread::run() {AVFrame *frame = av_frame_alloc();while (!isStopped) {if (m_frameQueue->size() > 10) {//                        qDebug()<<"解码线程等待";msleep(10);//            std::this_thread::sleep_for(std::chrono::milliseconds(10));continue;}AVPacket *pkt = m_packetQueue->pop(5);if (pkt) {int ret = avcodec_send_packet(dec_ctx, pkt);av_packet_free(&pkt);if (ret < 0) {qDebug() << "avcodec_send_packet error";break;}while (true) {ret = avcodec_receive_frame(dec_ctx, frame);if (ret == 0) {m_frameQueue->push(frame);//                    qDebug()<<"m_frameQueue size:"<<m_frameQueue->size();continue;} else if (AVERROR(EAGAIN) == ret) {break;} else {isStopped = true;qDebug() << "avcodec_receive_frame error";break;}}} else {break;}}
}
int DecodeThread::start() {isStopped = false;isPlaying = true;QThread::start();if (!currentThread()) {qDebug() << "线程创建失败";return -1;}return 0;
}
void DecodeThread::stop() {BaseThread::stop();if (dec_ctx)avcodec_close(dec_ctx);
}

测试是否能够正常运行

现在解复用线程模块和解码线程模块都已经完成了,测试一下是否正常运行

main.cpp

#include <QApplication>
#include <QPushButton>
//-----------
#include "queue/AVFrameQueue.h"
#include "queue/AVPacketQueue.h"
#include "thread/DecodeThread.h"
#include "thread/DemuxThread.h"int main(int argc, char *argv[]) {QApplication a(argc, argv);QPushButton button("Hello world!", nullptr);button.resize(200, 100);button.show();// 解复用DemuxThread *demuxThread;DecodeThread *audioDecodeThread;DecodeThread *videoDecodeThread;// 解码-音频AVPacketQueue audioPacketQueue;AVFrameQueue audioFrameQueue;// 解码-视频AVPacketQueue videoPacketQueue;AVFrameQueue videoFrameQueue;demuxThread = new DemuxThread(&audioPacketQueue, &videoPacketQueue);demuxThread->setUrl("/Users/mac/Downloads/23.mp4");demuxThread->start();int ret;audioDecodeThread = new DecodeThread(demuxThread->getCodec(MediaType::Audio),demuxThread->getCodecParameters(MediaType::Audio),&audioPacketQueue,&audioFrameQueue);audioDecodeThread->init();audioDecodeThread->start();videoDecodeThread = new DecodeThread(demuxThread->getCodec(MediaType::Video),demuxThread->getCodecParameters(MediaType::Video),&videoPacketQueue,&videoFrameQueue);videoDecodeThread->init();videoDecodeThread->start();return QApplication::exec();
}

测试完成运行正常

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亚马逊两步验证有哪些验证方法?

亚马逊通常提供多种两步验证的方式&#xff0c;包括短信&#xff08;通过手机接收验证码&#xff09;和认证器应用程序&#xff08;如Google Authenticator、Authy等&#xff09;。选择你偏好的方式。 短信验证&#xff1a; 如果选择短信验证&#xff0c;需要将你的手机号码关联…

YOLOv8改进 | 2023 | LSKAttention大核注意力机制助力极限涨点

论文地址&#xff1a;官方论文地址 代码地址&#xff1a;官方代码地址 一、本文介绍 在这篇文章中&#xff0c;我们将讲解如何将LSKAttention大核注意力机制应用于YOLOv8&#xff0c;以实现显著的性能提升。首先&#xff0c;我们介绍LSKAttention机制的基本原理&#xff0c;…

定制手机套餐---python序列

if __name__ __main__:print("定制手机套餐")print("")#定义电话时长&#xff1a;字典callTimeOptions{1:0分钟,2:50分钟,3:100分钟,4:300分钟,5:不限量}keyinput("请输入电话时长的选择编号&#xff1a;")valuecallTimeOptions.get(key)if val…

代码随想录算法训练营第五十四天|392.判断子序列 115.不同的子序列

文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;代码随想录B站账号 状态&#xff1a;看了视频题解和文章解析后做出来了 392.判断子序列 class Solution:def isSubsequence(self, s: str, t: str) -> bool:dp [[0] * (len(t)1) for _ in range(len(s)1)]for i in ra…

RabbitMq使用与整合

MQ基本概念 MQ概述 MQ全称 Message Queue&#xff08;[kjuː]&#xff09;&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 &#xff08;队列是一种容器&#xff0c;用于存放数据的都是容器&#xff0c;存…

优秀的时间追踪软件Timemator for Mac轻松管理时间!

在现代社会&#xff0c;时间管理成为了我们工作和生活中的一大挑战。如果你经常感到时间不够用&#xff0c;无法高效地完成任务&#xff0c;那么Timemator for Mac将成为你的得力助手。 Timemator for Mac是一款出色的时间追踪软件&#xff0c;它可以帮助你精确记录和管理你的…

Linux的基本指令 ( 一 )

目录 前言 Linux基本指令 快速认识五个指令 ls指令 补充内容 pwd指令 补充内容 cd指令 补充内容 重新认识指令 指令的本质 which指令 alias指令 最后 一个文件的三种时间 tree指令及安装 tree指令 前言 关于Linux操作系统的桌面&#xff0c;在学校教学中我们…

实用高效 无人机光伏巡检系统助力电站可持续发展

近年来&#xff0c;我国光伏发电行业规模日益壮大&#xff0c;全球领先地位愈发巩固。为解决光伏电站运维中的难题&#xff0c;浙江某光伏电站与复亚智能达成战略合作&#xff0c;共同推出全自动无人机光伏巡检系统&#xff0c;旨在提高发电效率、降低运维成本&#xff0c;最大…

Spark---SparkCore(一)

一、术语与宽窄依赖 1、术语解释 1、Master(standalone):资源管理的主节点&#xff08;进程&#xff09; 2、Cluster Manager:在集群上获取资源的外部服务(例如&#xff1a;standalone,Mesos,Yarn) 3、Worker Node(standalone):资源管理的从节点(进程)或者说管理本机资源的…

用Python写一个浏览器集群框架

更多Python学习内容&#xff1a;ipengtao.com 在分布式爬虫和大规模数据采集的场景中&#xff0c;使用浏览器集群是一种有效的方式&#xff0c;可以提高数据采集的速度和效率。本文将介绍如何用Python编写一个简单但强大的浏览器集群框架&#xff0c;以应对需要使用多个浏览器实…

WebGL/threeJS面试题扫描与总结

什么是 WebGL&#xff1f;什么是 Three.js&#xff1f;请解释three.js中的WebGL和Canvas的区别&#xff1f; WebGL(全写Web Graphics Library)是一种3D绘图协议&#xff0c;这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起&#xff0c;通过增加OpenGL ES 2.0的一个…

思科模拟器操作命令

模式 思科模拟器常见的模式有 用户模式 能够操作的命令比较少 特权模式特权模式下面可以操作的比较多 全局模式 接口模式 用户模式进入特权模式: 命令enable 特权模式进行全局模式命令: configure terminal 退出命令 exit命令&#xff1a;返回上一层&#xff0c;即一步一步…

RocketMQ 消息中间件 知识点汇总

目录 RocketMQ1、什么是RocketMQ?常用术语:2、为什么需要消息队列3、什么是异步处理4、什么是服务解耦5、什么是流量控制6、消息队列两种模型队列模型:发布/订阅模型:总结:7、怎么保证消息不丢失8、如何处理消息被重复消费**出现消息重复的情况:****解决方法:**9、如何保…

流量分析-PhishingEmail_WriteUp

一、题目问题 问题1&#xff1a;黑客的email名称 问题2&#xff1a;黑客向几人发送了钓鱼邮件 问题3&#xff1a;黑客传输的木马文件名 问题4&#xff1a;下载并运行了木马文件的人的email名称和ip地址&#xff0c;用“-”连接 问题5&#xff1a;黑客用于反弹shell的主机i…

什么葡萄酒会适用这种双重滗析方法呢?

滗析有两个主要目的&#xff0c;一种是去除陈年或未经过滤的葡萄酒中的沉淀物。虽然沉淀物不会对你造成任何伤害&#xff0c;但当喝葡萄酒满嘴都是葡萄沉淀物时是一件很糟糕的事。其次&#xff0c;倾析葡萄酒是可以让葡萄酒“呼吸”与氧气接触的&#xff0c;氧气可以软化单宁&a…

LeetCode Hot100 102.二叉树的层序遍历

题目&#xff1a; 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 方法&#xff1a;迭代 class Solution {public List<List<Integer>> levelOrder(TreeNode root) {if …

C语言——输入一个4位正整数,输出其逆数。

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int i,j 0;int a1,a2,a3,a4;printf("输入一个4位正整数&#xff1a;\n");scanf("%d",&i);a1 i/1000; a2 i/100%10; a3 i/10%10; a4 i%10; printf("千位a1%d,百位a…

80C51单片机----数据传送类指令

目录 一.一般传送指令&#xff0c;即mov指令 1.16位传送&#xff08;仅1条&#xff09; 2.8位传送 &#xff08;1&#xff09;目的字节为A&#xff08;累加器&#xff09; &#xff08;2&#xff09;目的字节为Rn(工作寄存器) &#xff08;3&#xff09;目的字节为direct…

超分辨率重建

意义 客观世界的场景含有丰富多彩的信息&#xff0c;但是由于受到硬件设备的成像条件和成像方式的限制&#xff0c;难以获得原始场景中的所有信息。而且&#xff0c;硬件设备分辨率的限制会不可避免地使图像丢失某些高频细节信息。在当今信息迅猛发展的时代&#xff0c;在卫星…

导入PIL时报错

在导入PIL时,报以下错误: 查找原因 参考博客 Could not find a version that satisfies the requirement PIL (from versions: ) No matching distributi-CSDN博客,按照wheel后,安装PIL时,报如下的错误。 查找说是python版本与wheel文件版本不同,确认本机python版本 …