js部分代码各类框架都适用,稍作改动即可,当前项目为react+Ant Design框架代码
<Dragger
{...uploadVideoProps}
fileList={videoList}
maxCount={1}
customRequest={customRequest}
accept={".mp4,.avi"}>
<p className="ant-upload-drag-icon">
<CloudUploadOutlined style={{ color: "rgba(0,0,0,.5)" }} />
</p>
<p className="ant-upload-text">
<a>上传视频</a>
</p>
</Dragger>
import SparkMD5 from 'spark-md5';
import { Upload } from "antd";
const { Dragger } = Upload;
// 一 、第一个函数(file前置处理函数)
const customRequest = async (params) => {
setcount(0)
setisOpen(false)
setVideoList([]);
const { file } = params // 拿到file
const { size, type, name } = file // 解构出可能用到的几个属性
setVideoList([{ name: name }])
//获取文件md5值(文件的md5值是根据文件内容生成的一个单独的标示,需要安装并引入一个插件:import SparkMD5 from 'spark-md5')
const result = await fileParse(file, 'buffer') // fileParse转换函数,将file转成buffer格式数据,
const spark = new SparkMD5.ArrayBuffer()
spark.append(result)
const hash = spark.end() // 获取到的文件md5值
// 文件切片上传
const partSize = 20 * 1024 * 1024 // 考虑到前后端上传效率,切成10M/片,这个可根据项目实际需要自己决定
const totalChunks = Math.ceil(file.size / partSize);//分片总数
//2 32 100
const share = size <= partSize ? 1 : size / partSize // 判断如果文件<=20M,不切片(或者不调用分片上传),超过按照10M每份切
let cur = 0
const partList = [] // 存储文件切片的数组
for (let i = 0; i < share; i++) {
const obj = {
file: file.slice(cur, cur + partSize), // 调用file.slice方法进行file的切片,第一个参数从哪里开始切,第二个参数切到哪里
id: i // 每一个切片的单独标示,后期续传时用与去除已上传部分的切片
}
partList.push(obj) // 把切的每一片统一放到一个数组里
cur += partSize
}
console.log(totalChunks, '文件大小');
console.log(partList, '分片总数');
//调用断点续传
register(partList, hash, name, totalChunks); // 此函数往下看👇
}
// 二 、 第二个函数(注册上传任务函数 -- 调用第一个接口,注册上传任务接口)
const register = (partList, hash, fileName, totalChunks) => {
// 1.调第一个接口,上传任务注册接口,参数需要将文件的md5传过去,后台专门根据此文件开启一个上传任务(一个文件对应一个上传任务)
demoUpload({ fileMd5: hash })
.then((res) => {
console.log(res);
const { fileObject, partResultList, taskStatus } = res.data // uploadId是与这个文件对应的上传任务的id标示,status是此文件的状态,如已上传部分切片||从未进行上传,这是第一次||已经上传完了还未进行合并,partResultList是一个数组,里面放着已经上传的切片的id,如果是从未进行上传,就是空数组
const mergeParame = { hash, fileName }
sessionStorage.setItem("isRemove", 1)
if (taskStatus == 'MERGE') { // 1.已全部上传,未合并,直接调合并函数
setisOpen(true)
complete(mergeParame)
} else if (taskStatus == 'PART') { // 2.已上传部分,根据后台返回的partResultList字段(已上传切片id的数组)筛选,去除已上传部分,把剩下的切片组成一个新数组,如果筛选出来的新数组长度是0说明切片都已上传,直接合并,否则调用上传切片函数
const newPartList = []
setisOpen(true)
let newArr = partResultList.map(obj => obj.chunk);
partList.forEach((item) => {
if (!newArr.includes(item.id)) {
newPartList.push(item)
}
})
if (newPartList.length === 0) {
complete(mergeParame)
} else {
console.log(newPartList, 'newPartList,续传数组');
newRequestArr(newPartList, hash, mergeParame, totalChunks)
}
} else if (taskStatus == 'NOT_UPLOADED') { // 3.从未上传过此文件,调用上传切片函数
setisOpen(true)
newRequestArr(partList, hash, mergeParame, totalChunks)
} else if (taskStatus == 'UPLOADED') { //已合并已上传
message.success('上传成功')
setcount(100)
setisOpen(false)
setVideoList([{ ...fileObject, name: fileObject.fileName }])
}
})
}
// 三 、 第三个函数(上传切片函数 -- 调用第二个接口,上传文件切片接口)
const newRequestArr = (partList, hash, mergeParame, totalChunks) => {
let i = 0
const fn = () => {
const formData = new FormData() // 通过form将file切片传给后台
formData.append('chunk', partList[i].id);
formData.append('file', partList[i].file);
formData.append('chunks', totalChunks);
formData.append('sourceFileMd5', hash);
partUpload(formData).then((res) => { //分片上传接口
// console.log(res,'分片上传接口');
if (res.code == '-1') {
message.error('网络异常请,请重试!')
setisOpen(false)
sessionStorage.setItem("isRemove", 0)
setcount(0)
setVideoList([]);
}
if (sessionStorage.getItem("isRemove") == 1) {
if (res.status == 1) { // 如果不等于-1,则会停止递归调用上传函数,至于在哪里把他置为false,看你在哪里点击触发的暂停或者删除上传函数了,自己决定
setcount((res.data.chunk / (totalChunks - 1)) * 96)
i += 1
if (i < partList.length) {
fn()
} else {
complete(mergeParame) // 如果是最后一个切片,则调用合并函数
}
} else {
setisOpen(false)
sessionStorage.setItem("isRemove", 0)
setcount(0)
setVideoList([]);
message.error('上传失败,请重试!')
return
}
}
})
}
fn()
}
// 四 、 第四个函数(文件合并函数 -- 调用第三个接口,文件合并接口)
const complete = (mergeParame) => {
// console.log(mergeParame);
let params = {
fileMd5: mergeParame.hash,
fileName: mergeParame.fileName
}
fileMerge(params).then((res) => {//合并分片接口
const { status, data } = res
if (res.status == 1) {
message.success('上传成功')
setcount(100)
setVideoList([{ ...data, name: data.fileName }])
setisOpen(false)
return
} else {
setisOpen(false);
setcount(0);
setVideoList([]);
message.error("上传失败,请重试!");
return;
}
})
}
// 转换函数
const fileParse = (file, type = 'base64') => {
return new Promise((res) => {
const fileReader = new FileReader()
/* fileReader.readAsArrayBuffer() // 转成buffer格式数据
fileReader.readAsBinaryString() // 转成二进制格式数据
fileReader.readAsDataURL() // 转成base64格式数据
解析过程是异步,所以需要调用onload事件的e.target.result获取转换后的结果
*/
switch (type) {
case 'base64':
fileReader.readAsDataURL(file)
break
case 'buffer':
fileReader.readAsArrayBuffer(file)
break
case 'binary':
fileReader.readAsBinaryString(file)
break
default:
break
}
fileReader.onload = (e) => {
res(e.target?.result)
}
})
}
// 大视频上传
const uploadVideoProps = {
name: "file",
action: apiHost + "/zjkc/file/uploadFile",
multiple: true,
showUploadList: true,
accept: ".mp4",
async beforeUpload(file) {
console.log(file);
if (file) {
const isLt2G = file.size / 1024 / 1024 / 1024 < 2;
// console.log(file,'+++ 视频大小++++');
if (!['video/mp4'].includes(file.type)) {
message.warning('请上传.mp4格式视频');
return Upload.LIST_IGNORE;
}
if (!isLt2G) {
message.warning('视频大小不超过2GB');
return Upload.LIST_IGNORE;
}
const maxDuration = 15 * 60;
const videoDuration = await checkVideoDuration(file, maxDuration)
if (!videoDuration) {
message.warning('上传的视频应在15分钟内');
return Upload.LIST_IGNORE;
}
return true
}
}
//视频时长--用于限制上传时长
const checkVideoDuration = (file, t) => {
return new Promise((resolve, reject) => {
const videoUrl = URL.createObjectURL(file)
const videoObj = document.createElement('video')
videoObj.preload = 'metadata'
videoObj.src = videoUrl
videoObj.onloadedmetadata = () => {
URL.revokeObjectURL(videoUrl)
let times = Math.round(videoObj.duration)
console.log(times, 'times')
if (parseFloat(times) > t) {
resolve(false);
} else {
resolve(true);
}
}
});
}