当前位置: 首页 > news >正文

视频分块上传Vue3+SpringBoot3+Minio

文章目录

  • 一、简化演示
      • 分块上传、合并分块
      • 断点续传
      • 秒传
  • 二、更详细的逻辑和细节问题
      • 可能存在的隐患
  • 三、代码示例
      • 前端代码
      • 后端代码

一、简化演示

分块上传、合并分块

前端将完整的视频文件分割成多份文件块,依次上传到后端,后端将其保存到文件系统。前端将文件块上传完毕后,发送合并请求,后端拿取文件块,合并后重新上传到文件系统。
在这里插入图片描述

断点续传

前端遍历文件块,每次上传之前,先询问文件块是否存在,只有不存在的情况下,才会上传。
请添加图片描述

秒传

前端分割视频文件前,先询问此视频是否已经存在,存在则不再上传,后端之间返回视频信息。前端看起来就像是被秒传了。
请添加图片描述

二、更详细的逻辑和细节问题

  • 视频文件和文件块都通过文件本身计算MD5值作为唯一标志
  • 文件系统使用Minio,只要提供buckerNamepath就可以操作文件
  • 后端合并文件块成功后会删除文件块,并以MD5值为id存入数据库
  • Minio存储文件块时,依据其md5值计算path,比如取前两个字符构建二级文件夹,文件名为md5值,无后缀。所以只需要提供文件块的md5值就可以操作文件块。
  • Minio存储完整视频文件时,依据其md5值计算path,同上,文件名为md5值,携带.mp4等后缀,所以只需要提供视频文件的md5值就可以操作视频文件。
  1. 首先,前端计算视频文件的MD5值,记为fileMd5,传递MD5值来询问后端此视频文件是否存在,后端查询数据库返回结果,如果存在,则前端触发“秒传”。
  2. 如果不存在,则将视频文件分割成文件块,循环上传,每次循环,首先计算文件块的md5值,传递md5值询问后端此文件块是否存在,后端根据md5判断文件块是否存在,如果存在,前端跳过此文件块上传,直接标记为上传成功,如果不存在,则上传至后端,后端将其保存到minio。这其实就是“分块上传,断点续传”。
  3. 最后所有分块文件都上传成功,前端发起合并请求,传递视频文件的md5值和所有文件块的md5值到后端,后端进行文件块合并、文件块的删除、合并文件的上传,将信息存储在mysql数据库,将执行结果告知前端。这就是“合并分块”

可能存在的隐患

一个视频文件的文件块没有全部上传完成就终止,此时文件块将一直保存在minio中,如果之后此视频再也没有发起过上传请求,那么这些文件块都是是一种垃圾。

可以写一个定时任务,遍历Minio没有后缀的文件块,判断其创建时间距离当前是否足够久,是则删除。

三、代码示例

前端代码

<template><div class="p-2"><el-button icon="Plus" plain type="primary" @click="handleAdd">新增</el-button><!-- 添加或修改media对话框 --><el-dialog v-model="dialog.visible" :title="dialog.title" append-to-body width="500px"><el-form ref="mediaFormRef" :model="form" :rules="rules" label-width="80px"><el-form-item label="上传视频" prop="originalName" v-show="dialog.title=='添加视频'"><el-uploadref="uploadRef":http-request="onUpload":before-upload="beforeUpload":limit="1"action="#"class="upload-demo"><template #trigger><el-button type="primary">选择视频</el-button></template><template #tip><div class="el-upload__tip">支持分块上传、端点续传</div></template></el-upload></el-form-item><el-form-item v-show="percentageShow"><el-progress :percentage="percentage" style="width: 100%"/></el-form-item></el-form></el-dialog></div>
</template><script lang="ts" name="Media" setup>
import type {UploadInstance, UploadRawFile, UploadRequestOptions, UploadUserFile} from 'element-plus'
import SparkMD5 from "spark-md5";
import {HttpStatus} from "@/enums/RespEnum";const dialog = reactive<DialogOption>({visible: false,title: ''
});
//上传视频
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + "/media/media/image"); // 上传的图片服务器地址
const uploadRef = ref<UploadInstance>()
const needUpload = ref(true)
const chunkSize = 5*1024*1024;const percentage = ref(0)
const percentageShow = ref(false)/** 新增按钮操作 */
const handleAdd = () => {dialog.visible = true;dialog.title = "添加视频";percentageShow.value = false;
}//获取文件的MD5
const getFileMd5 = (file:any) => {return new Promise((resolve, reject) => {let fileReader = new FileReader()fileReader.onload = function (event) {let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)resolve(fileMd5)}fileReader.readAsArrayBuffer(file)})
}//在上传之前,使用视频md5判断视频是否已经存在
const beforeUpload = async (rawFile: UploadRawFile) => {needUpload.value = true;const fileMd5 = await getFileMd5(rawFile);form.value.id = fileMd5;const rsp = await getMedia(fileMd5);if(!!rsp.data && rsp.data['id'] == fileMd5){needUpload.value = false;proxy?.$modal.msgWarning("视频文件已存在,请勿重复上传。文件名为"+rsp.data['originalName'])}
}//分块上传、合并分块
const onUpload = async (options: UploadRequestOptions) => {if(!needUpload.value){//秒传percentageShow.value = true;percentage.value = 100;dialog.visible = false;return;}percentageShow.value = true;const file = options.fileconst totalChunks = Math.ceil(file.size / chunkSize);let isUploadSuccess = true;//记录分块文件是否上传成功//合并文件参数let mergeVo = {"chunksMd5": [] as string[],"videoMd5": undefined as string | undefined,"videoName": file.name,"videoSize": file.size,"remark": undefined as string | undefined}//循环切分文件,并上传分块文件for(let i=0; i<totalChunks; ++i){const start = i * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);//计算 chunk md5const md5 = await getFileMd5(chunk);mergeVo.chunksMd5.push(md5);// 准备FormDataconst formData = new FormData();formData.append('file', chunk);formData.append('filename', file.name);formData.append('chunkIndex', i.toString());formData.append('totalChunks', totalChunks.toString());formData.append('md5', md5);//上传当前分块try {//先判断这个分块是否已经存在const isExistRsp = await isChunkExist({"md5": formData.get("md5")});const isExist = isExistRsp.data;//不存在则上传if (!isExist){const rsp = await addChunk(formData);console.log(`Chunk ${i + 1}/${totalChunks} uploaded`, rsp.data);}else {console.log(`Chunk ${i + 1}/${totalChunks} is exist`);}percentage.value = (i)*100 / totalChunks;} catch (error) {isUploadSuccess = false;console.error(`Error uploading chunk ${i + 1}`, error);proxy?.$modal.msgError(`上传分块${i + 1}出错`);break;}}//合并分块文件if(isUploadSuccess){proxy?.$modal.msgSuccess("分块文件上传成功")mergeVo.videoMd5 = form.value.id;//beforeUpload已经计算过视频文件的md5//合并文件const rsp = await mergeChunks(mergeVo);if (rsp.code == HttpStatus.SUCCESS){//合并文件后,实际上媒资已经插入数据库。percentage.value = 100;proxy?.$modal.msgSuccess("文件合并成功")proxy?.$modal.msgSuccess("视频上传成功")}else{proxy?.$modal.msgSuccess("文件合并异常")}}else {proxy?.$modal.msgSuccess("文件未上传成功,请重试或联系管理员")}
}</script>
export const getMedia = (id: string | number): AxiosPromise<MediaVO> => {return request({url: '/media/media/' + id,method: 'get'});
};/*** 分块文件是否存在* */
export const isChunkExist = (data: any) => {return request({url: '/media/media/video/chunk',method: 'get',params: data});
};/*** 上传分块文件* */
export const addChunk = (data: any) => {return request({url: '/media/media/video/chunk',method: 'post',data: data});
};/*** 合并分块文件* */
export const mergeChunks = (data: any) => {return request({url: '/media/media/video/chunk/merge',method: 'post',data: data});
};

后端代码

@RestController
@RequestMapping("/media")
public class MediaFilesController extends BaseController {/*** 获取media详细信息** @param id 主键*/@GetMapping("/{id}")public R<MediaFilesVo> getInfo(@NotNull(message = "主键不能为空")@PathVariable String id) {return R.ok(mediaFilesService.queryById(id));}@Log(title = "视频分块文件上传")@PostMapping(value = "/video/chunk")public R<String> handleChunkUpload(@RequestParam("file") MultipartFile file,@RequestParam("md5") String md5,@RequestParam("filename") String filename,@RequestParam("chunkIndex") int chunkIndex,@RequestParam("totalChunks") int totalChunks) {if (ObjectUtil.isNull(file)) {return R.fail("上传文件不能为空");}Boolean b = mediaFilesService.handleChunkUpload(file, md5);if (b){return R.ok();}else {return R.fail();}}@Log(title = "分块文件是否已经存在")@GetMapping(value = "/video/chunk")public R<Boolean> isChunkExist(@RequestParam("md5") String md5) {return R.ok(mediaFilesService.isChunkExist(md5));}@Log(title = "合并视频文件")@PostMapping(value = "/video/chunk/merge")public R<Boolean> mergeChunks(@RequestBody MediaVideoMergeBo bo) {bo.setCompanyId(LoginHelper.getDeptId());Boolean b = mediaFilesService.mergeChunks(bo);if (b){return R.ok();}else {return R.fail();}}
}

关于如何操作Minio等文件系统,不详细写明解释。只需要知道,给Minio提供文件本身、bucketName、path即可完成上传、下载、删除等操作。具体代码不同的包都不一样。

@Service
public class MediaFilesServiceImpl implements MediaFilesService {@Autowiredprivate MediaFilesMapper mediaFilesMapper;/*** 分块文件上传* <br/>* 分块文件不存放mysql信息,同时文件名不含后缀,只有md5* @param file 文件* @param md5  md5* @return {@link Boolean}*/@Overridepublic Boolean handleChunkUpload(MultipartFile file, String md5) {//只上传至minioOssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");try {storage.upload(file.getInputStream(), path, file.getContentType(), minioProperties.getVideoBucket());} catch (IOException e) {throw new RuntimeException(e);}return true;}@Overridepublic Boolean isChunkExist(String md5) {OssClient storage = OssFactory.instance();String path = getPathByMD5(md5, "");return storage.doesFileExist(minioProperties.getVideoBucket(), path);}@Overridepublic Boolean mergeChunks(MediaVideoMergeBo bo) {OssClient storage = OssFactory.instance();String originalfileName = bo.getVideoName();String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());//创建临时文件,用来存放合并文件String tmpDir = System.getProperty("java.io.tmpdir");String tmpFileName = UUID.randomUUID().toString() + ".tmp";File tmpFile = new File(tmpDir, tmpFileName);try(FileOutputStream fOut = new FileOutputStream(tmpFile);) {//将分块文件以流的形式copy到临时文件List<String> chunksMd5 = bo.getChunksMd5();chunksMd5.forEach(chunkMd5 -> {String chunkPath = getPathByMD5(chunkMd5, "");InputStream chunkIn = storage.getObjectContent(minioProperties.getVideoBucket(), chunkPath);IoUtil.copy(chunkIn, fOut);});//合并文件上传到minioString videoMd5 = bo.getVideoMd5();String path = getPathByMD5(videoMd5, suffix);storage.upload(tmpFile, path, minioProperties.getVideoBucket());//删除分块文件chunksMd5.forEach(chunkMd5->{String chunkPath = getPathByMD5(chunkMd5, "");storage.delete(chunkPath, minioProperties.getVideoBucket());});} catch (Exception e) {throw new RuntimeException(e);}finally {if (tmpFile.exists()){tmpFile.delete();}}//上传信息到mysqlMediaFiles mediaFiles = new MediaFiles();mediaFiles.setId(bo.getVideoMd5());mediaFiles.setCompanyId(bo.getCompanyId());mediaFiles.setOriginalName(originalfileName);mediaFiles.setFileSuffix(suffix);mediaFiles.setSize(bo.getVideoSize());mediaFiles.setPath(getPathByMD5(bo.getVideoMd5(), suffix));mediaFiles.setRemark(bo.getRemark());mediaFiles.setAuditStatus(MediaStatusEnum.UNREVIEWED.getValue());return mediaFilesMapper.insert(mediaFiles) > 0;}/*** 通过md5生成文件路径* <br/>* 比如* md5 = 6c4acb01320a21ccdbec089f6a9b7ca3* <br/>* path = 6/c/md5 + suffix* @param prefix 前缀* @param suffix 后缀* @return {@link String}*/public String getPathByMD5(String md5, String suffix) {// 文件路径String path = md5.charAt(0) + "/" + md5.charAt(1) + "/" + md5;return path + suffix;}}

相关文章:

视频分块上传Vue3+SpringBoot3+Minio

文章目录 一、简化演示分块上传、合并分块断点续传秒传 二、更详细的逻辑和细节问题可能存在的隐患 三、代码示例前端代码后端代码 一、简化演示 分块上传、合并分块 前端将完整的视频文件分割成多份文件块&#xff0c;依次上传到后端&#xff0c;后端将其保存到文件系统。前…...

深入浅出 -- 系统架构之单体到分布式架构的演变

一、传统模式的技术改革 在很多年以前&#xff0c;其实没有严格意义上的前后端工程师之分&#xff0c;每个后端就是前端&#xff0c;同理&#xff0c;前端也可以是后端&#xff0c;即Ajax、jQuery技术未盛行前的年代。 起初&#xff0c;大部分前端界面很简单&#xff0c;显示的…...

每日一题 第七十期 洛谷 [蓝桥杯 2020 省 AB2] 回文日期

[蓝桥杯 2020 省 AB2] 回文日期 题目描述 2020 年春节期间&#xff0c;有一个特殊的日期引起了大家的注意&#xff1a;2020 年 2 月 2 日。因为如果将这个日期按 yyyymmdd 的格式写成一个 8 8 8 位数是 20200202&#xff0c;恰好是一个回文数。我们称这样的日期是回文日期。…...

蓝桥杯第十四届C++A组(未完)

【规律题】平方差 题目描述 给定 L, R&#xff0c;问 L ≤ x ≤ R 中有多少个数 x 满足存在整数 y,z 使得 。 输入格式 输入一行包含两个整数 L, R&#xff0c;用一个空格分隔。 输出格式 输出一行包含一个整数满足题目给定条件的 x 的数量。 样例输入 1 5 样例输出 …...

职场口才提升之道

职场口才提升之道 在职场中&#xff0c;口才的重要性不言而喻。无论是与同事沟通协作&#xff0c;还是向上级汇报工作&#xff0c;亦或是与客户洽谈业务&#xff0c;都需要具备良好的口才能力。一个出色的职场人&#xff0c;除了拥有扎实的专业技能外&#xff0c;还应具备出色…...

【算法练习】28:选择排序学习笔记

一、选择排序的算法思想 弄懂选择排序算法&#xff0c;先得知道两个概念&#xff1a;未排序序列&#xff0c;已排序序列。 原理&#xff1a;以升序为例&#xff0c;选择排序算法的思想是&#xff0c;先将整个序列当做未排序的序列&#xff0c;以序列的第一个元素开始。然后从左…...

【关于窗口移动求和的两种计算方法】

窗口移动计算方法 例子方法1方法2运行结果: 例子 在很多算法中都会涉及到窗口滑动&#xff0c;比如基于新息序列更新的自适应卡尔曼滤波器算法中便会使用到。 已知一个数列&#xff1a;OCV [1;2;3;4;5;6;7;8;9;10;11;12;13;14;15]&#xff0c;定义窗口长度为5&#xff0c;每次…...

Win10文件夹共享(有密码的安全共享)(SMB协议共享)

前言 局域网内&#xff08;无安全问题&#xff0c;比如自己家里wifi&#xff09;无密码访问&#xff0c;参考之前的操作视频 【电脑文件全平台共享、播放器推荐】手机、电视、平板播放硬盘中的音、视频资源 下面讲解公共网络如办公室网络、咖啡厅网络等等环境下带密码的安全…...

Client sent an HTTP request to an HTTPS server

背景 最近踩坑了 我发现域名&#xff1a;8000可以访问我的服务 但是域名&#xff1a;443却不行&#xff0c;这很反常 结果发现是nginx配置的问题&#xff0c;需要把http改成https&#xff01; 原因 如果你的后端服务&#xff08;运行在8000端口上&#xff09;已经配置了SS…...

Springboot传参要求

Web.java(这里定义了一个实体类交Web) public class Web{ private int Page; public int getPage() {return Page;}public void setPage(int page) {Page page;} } 1、通过编译器自带的getter、Setter传参 。只是要注意参数的名字是固定的&#xff0c;不能灵活改变。 传参的…...

数字乡村创新实践探索:科技赋能农业现代化与乡村治理体系现代化同步推进

随着信息技术的飞速发展&#xff0c;数字乡村作为乡村振兴的重要战略方向&#xff0c;正日益成为推动农业现代化和乡村治理体系现代化的关键力量。科技赋能下的数字乡村&#xff0c;不仅提高了农业生产的效率和品质&#xff0c;也为乡村治理带来了新的机遇和挑战。本文旨在探讨…...

C语言——找单身狗1

题目描述&#xff1a; 在一个整形数组中&#xff0c;只有一个数字出现一次&#xff0c;其他数组都是成对出现的&#xff0c;找出那个只出现一次的数字。 例如&#xff1a; 数组中&#xff1a;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;4&#xff0c;3…...

Day82:服务攻防-开发组件安全Solr搜索Shiro身份Log4j日志本地CVE环境复现

目录 J2EE-组件Solr-本地demo&CVE 命令执行&#xff08;CVE-2019-17558&#xff09; 远程命令执行漏洞(CVE-2019-0193) Apache Solr 文件读取&SSRF (CVE-2021-27905) J2EE-组件Shiro-本地demo&CVE CVE_2016_4437 Shiro-550Shiro-721(RCE) CVE-2020-11989(身…...

网络协议——VRRP(虚拟路由冗余协议)原理与配置

1. VRRP概述 单网关出现故障后下联业务中断&#xff0c;配置两个及以上的网关时由于IP地址冲突&#xff0c;导致通讯时断时续甚至通信中断。VRRP组播类的网络层协议 2. 协议版本 VRRP v2: 支持认证,仅适用于IPv4网络 VRRP v3: 不支持认证&#xff0c; 适用于IPv4和IPv6两种网…...

Elasticsearch:我们如何演化处理二进制文档格式

作者&#xff1a;来自 Elastic Sean Story 从二进制文件中提取内容是一个常见的用例。一些 PDF 文件可能非常庞大 — 考虑到几 GB 甚至更多。Elastic 在处理此类文档方面已经取得了长足的进步&#xff0c;今天&#xff0c;我们很高兴地介绍我们的新工具 —— 数据提取服务&…...

第八讲 Sort Aggregate 算法

我们现在将讨论如何使用迄今为止讨论过的 DBMS 组件来执行查询。 1 查询计划【Query Plan】 我们首先来看当一个查询【Query】被解析【Parsed】后会发生什么&#xff1f; 当 SQL 查询被提供给数据库执行引擎&#xff0c;它将通过语法解析器进行检查&#xff0c;然后它会被转换…...

clickhouse MPPDB数据库--新特性使用示例

clickhouse 新特性&#xff1a; 从clickhouse 22.3至最新的版本24.3.2.23&#xff0c;clickhouse在快速发展中&#xff0c;每个版本都增加了一些新的特性&#xff0c;在数据写入、查询方面都有性能加速。 本文根据clickhouse blog中的clickhouse release blog中&#xff0c;学…...

MATLAB多级分组绘图及图例等细节处理 ; MATLAB画图横轴时间纵轴数值按照不同sensorCode分组画不同sensorCode的曲线

平时研究需要大量的绘图Excel有时候又臃肿且麻烦 尤其是当处理大量数据时可能会拖死Windows 示例代码及数据量展示 因为数据量是万级别的折线图也变成"柱状图"了, 不过还能看出大致趋势! 横轴是时间纵轴是传感器数值图例是传感器所在深度 % data readtable(C:\U…...

20240405,数据类型,运算符,程序流程结构

是我深夜爆炸&#xff0c;不能再去补救C了&#xff0c;真的来不及了&#xff0c;不能再三天打鱼两天晒网了&#xff0c;真的来不及了呜呜呜呜 我实在是不知道看什么课&#xff0c;那黑马吧……MOOC的北邮的C正在进行呜呜 #include <iostream> using namespace std; int…...

Prometheus+grafana环境搭建Nginx(docker+二进制两种方式安装)(六)

由于所有组件写一篇幅过长&#xff0c;所以每个组件分一篇方便查看&#xff0c;前五篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabbitmq(docker二进制两种方式安装)(二)-CSDN博客 Prometheusgrafana环…...

51c自动驾驶~合集58

我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留&#xff0c;CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制&#xff08;CCA-Attention&#xff09;&#xff0c;…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南

文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)

上一章用到了V2 的概念&#xff0c;其实 Fiori当中还有 V4&#xff0c;咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务)&#xff0c;代理中间件&#xff08;ui5-middleware-simpleproxy&#xff09;-CSDN博客…...

C++.OpenGL (20/64)混合(Blending)

混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...

Qemu arm操作系统开发环境

使用qemu虚拟arm硬件比较合适。 步骤如下&#xff1a; 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载&#xff0c;下载地址&#xff1a;https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...

【Veristand】Veristand环境安装教程-Linux RT / Windows

首先声明&#xff0c;此教程是针对Simulink编译模型并导入Veristand中编写的&#xff0c;同时需要注意的是老用户编译可能用的是Veristand Model Framework&#xff0c;那个是历史版本&#xff0c;且NI不会再维护&#xff0c;新版本编译支持为VeriStand Model Generation Suppo…...

rknn toolkit2搭建和推理

安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 &#xff0c;不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源&#xff08;最常用&#xff09; conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...