分片上传,断点上传 控制并发数量

总结

本次写的视频上传 想着大文件上传比较耗费性能就去学习了一下分片上传和断点上传
样式如下在这里插入图片描述
这是后端给的接口
在这里插入图片描述

实现分片上传

上传方法

const uploadFileViedo = async (event: any) => {
  
  if (event.target.files[0]) {
    if (event.target.files[0].type != "video/mp4") {
      ElMessage.error("请您上传视频文件");
      return;
    } else {
      const files = event.target.files[0];
      let maxSize = 1024 * 1024 * 1;
      const { hashArr, suffix } = await getHash(files);
      let count = Math.ceil(files.size / maxSize);
      //有多少需要上传的文件
      if (count > 100) {
        count = 100;
        maxSize = files.size / count;
      }
      const chunk = await spliceViedo(count, files, maxSize, hashArr, suffix);

      const restArr = await getrestStarIndexArr(hashArr);
      const restFilesIndexarr = restArr.data;

       for(let i=0;i<chunk.length;i++){
        const fm = new FormData()
        fm.append('file',chunk[i].file)
        uploadFileonce(fm,i,hashArr,count).then((data:any)=>{
          if(data.code==20000){
            uploadFlagMethods(chunk.length,files.name,hashArr,files);
          }else{
            ElMessage.error('上传失败请重试')
            fileUpload.value=null
          }
        })
       }
      console.log(restFilesIndexarr);

  
      loading.value = true;
      fileUpload.value = event.target.files[0];
    }
  }
};

spark-md5生成独一无二的hash串

import SparkMD5 from "spark-md5"
export function getHash(file:File):Promise<any>{
    return new Promise((resolve,reject)=>{
        //通过sparkmd5 得到hash值 先获取buffer
        const fileReader=new FileReader()  
        fileReader.readAsArrayBuffer(file)
        fileReader.onload=(e)=>{
            const buffer=e.target?.result
            if(!buffer) reject()
            const hashArr=new SparkMD5.ArrayBuffer().append(buffer as ArrayBuffer).end()
            //获取文件后缀
            const suffix = (/\.([a-zA-Z0-9]+)$/.exec(file.name) as any)[1]
            resolve({
                hashArr,
                suffix
            })
        }
    })
}

返回hashArr和文件获取文件后缀suffix

const { hashArr, suffix } = await getHash(files);

分割文件

先判断有多少上传的文件 如果超过100就采用100进行分片

    const files = event.target.files[0];
      let maxSize = 1024 * 1024 * 1;
      const { hashArr, suffix } = await getHash(files);
      let count = Math.ceil(files.size / maxSize);
      //有多少需要上传的文件
      if (count > 100) {
        count = 100;
        maxSize = files.size / count;
      }

切片的方法 返回切片的后的bolb数组

export async function spliceViedo(count:number,file:File,maxSize: number,HASH: any,suffix: any){
    let index=0
    let chunks: any[]=[]
    while(index<count){
        const sliceFile=file.slice(index*maxSize,(index+1)*maxSize)
        chunks.push({
            file :sliceFile,
            name: `${HASH}_${index}.${suffix}`
        })
        index++;
    }
    return chunks
}

不断地循环 然后调取函数判断上传的count达没达到分片数量 达到之后 采用合并文件接口

const uploadFlagMethods = (name: any, md5: any, files: any) => {
  // 完成 合并
  mergerFiles(name, md5)
    .then(async (data: any) => {
      if (data.code === 20000) {
        ElMessage.success("文件上传成功");
        loading.value = false;
        //截取视频帧数
        const list = await splicePhotoList(files);
        const obj = {
          md5: md5,
          url: data.data,
          name: name,
          list: list,
        };
        hasFilesUploaded.push(obj);
      } else {
        ElMessage.error("文件上传失败");
        loading.value = false;
      }
      uploadcount.value = 0;
    })
    .catch((err) => {
      console.log(err);
    });
};

断点续传

断点续传:当因为网络问题…中断上传任务的时候 下次上传的时候可以从未上传的索引处开始上传
在上传文件的时候 调取后端接口获取未上传的索引值

let restFilesIndexarr = reactive([]);
   const restArr = await getrestStarIndexArr(hashArr);
      const restFilesIndexarr = restArr.data;
        const asyncFunctions = chunk.map((file, index) => {
        return async () => {
          const fm = new FormData();
          fm.append("file", chunk[index].file);
          if (
            restFilesIndexarr &&
            restFilesIndexarr.length != 0 &&
            restFilesIndexarr !== null
          ) {
            if (restFilesIndexarr.includes(index)) {
              return uploadFileonce(fm, index, hashArr, count);
            }
          } else {
            return uploadFileonce(fm, index, hashArr, count);
          }
        };
      });

如果该索引存在于这个未上传的索引数组中 则继续上传
否则 跳过

控制并发数量

发送异步请求的时候 会开辟多个线程 但是这样极其耗费性能 所以控制并发数量是很有必要的
BehaviorSubject导入这个 对队列在进行的任务数量进行限制

import {BehaviorSubject} from 'rxjs'
type AsyncFunction=()=>Promise<any>
export class PromisePool{
    private readonly queue: { fn: AsyncFunction, index: number }[] = []
    private readonly maxCountcurrentTasks:number
    private results:any[]=[]
    curRunningCount =new BehaviorSubject(0)
    constructor(
        functions:AsyncFunction[],
        maxCountcurrentTasks:number=navigator.hardwareConcurrency||8
    ){
        this.queue = functions.map((fn, index) => ({ fn, index }))
        this.maxCountcurrentTasks=maxCountcurrentTasks
    }
    exec<T>(){
        return new Promise<T[]>((rs)=>{
            this.curRunningCount.subscribe((count: number)=>{
                if(count<this.maxCountcurrentTasks && this.queue.length!==0){
                    //需要跑的任务数量
                    let curTaskcount=this.maxCountcurrentTasks-count
                    if(curTaskcount>this.queue.length){
                        curTaskcount=this.queue.length
                    }
                    const tasks=this.queue.splice(0,curTaskcount)
                    this.curRunningCount.next(this.curRunningCount.value+curTaskcount)
                    tasks.forEach((taskWrap)=>{
                        const {fn ,index}=taskWrap
                        fn().then((result)=>{
                            this.results[index]=result
                        }).catch((error)=>{
                            this.results[index]=error
                        }).finally(()=>{
                            this.curRunningCount.next(this.curRunningCount.value-1)
                        })

                    })
                }
                if(this.curRunningCount.value==0 && this.queue.length==0){
                    rs(this.results as T[])
                }
            })
        })
    }
}

使用

当全部上传完在调用合并的接口

    const asyncFunctions = chunk.map((file, index) => {
        return async () => {
          const fm = new FormData();
          fm.append("file", chunk[index].file);
          if (
            restFilesIndexarr &&
            restFilesIndexarr.length != 0 &&
            restFilesIndexarr !== null
          ) {
            if (restFilesIndexarr.includes(index)) {
              return uploadFileonce(fm, index, hashArr, count);
            }
          } else {
            return uploadFileonce(fm, index, hashArr, count);
          }
        };
      });

      // // 创建PromisePool实例
      const pool = new PromisePool(asyncFunctions, 5);

      // // 执行上传任务
      try {
        const results = await pool.exec();
        results.forEach((result: any) => {
          if (result.code == !20000) {
            ElMessage.error("您上传的文件没有成功,请重试");
            return;
          }
        });
        uploadFlagMethods(files.name, hashArr, files);
        // 处理上传结果
      } catch (error) {
        // 处理执行过程中的错误
      }
      loading.value = true;
      fileUpload.value = event.target.files[0];
    }

这样达到的效果就是正在进行的并发数量为5
全部代码如下

<template>
  <div class="viedouploadMain">
    <div class="viedoUploadMaincenter" v-show="!hasUploaded">
      <div class="viedoUploadIcon">
        <svg
          t="1713964310959"
          class="icon"
          viewBox="0 0 1024 1024"
          version="1.1"
          xmlns="http://www.w3.org/2000/svg"
          p-id="1701"
          width="100"
          height="100"
        >
          <path
            d="M925.696 384q19.456 0 37.376 7.68t30.72 20.48 20.48 30.72 7.68 37.376q0 20.48-7.68 37.888t-20.48 30.208-30.72 20.48-37.376 7.68l-287.744 0 0 287.744q0 20.48-7.68 37.888t-20.48 30.208-30.72 20.48-37.376 7.68q-20.48 0-37.888-7.68t-30.208-20.48-20.48-30.208-7.68-37.888l0-287.744-287.744 0q-20.48 0-37.888-7.68t-30.208-20.48-20.48-30.208-7.68-37.888q0-19.456 7.68-37.376t20.48-30.72 30.208-20.48 37.888-7.68l287.744 0 0-287.744q0-19.456 7.68-37.376t20.48-30.72 30.208-20.48 37.888-7.68q39.936 0 68.096 28.16t28.16 68.096l0 287.744 287.744 0z"
            p-id="1702"
            fill="#bfbfbf"
          ></path>
        </svg>
        <p>
          <button>点击我上传视频</button>
        </p>
      </div>
      <input type="file" class="fileUpload" @change="uploadFileViedo" />
    </div>
  </div>
  <div class="uploadViedoList">
    <div class="topListViedoShowCenter">
      <div
        :class="{ onceUploadFilesflage: index == 0 }"
        class="onceUploadFiles"
        v-for="(item, index) in hasFilesUploaded"
        :key="index"
      >
        <div class="onceUploadFileCenter">
          <p>{{ item.name }}</p>
          <div class="uploadwordsFiles">
            <div class="uploadFilesSucess">
              <svg
                t="1714032800800"
                class="icon"
                viewBox="0 0 1024 1024"
                version="1.1"
                xmlns="http://www.w3.org/2000/svg"
                p-id="1676"
                width="15"
                height="15"
              >
                <path
                  d="M512 960c-247.424 0-448-200.576-448-448 0-247.424 200.576-448 448-448 247.424 0 448 200.576 448 448C960 759.424 759.424 960 512 960L512 960zM512 163.584C319.552 163.584 163.584 319.552 163.584 512c0 192.448 155.968 348.48 348.416 348.48 192.448 0 348.416-156.032 348.416-348.416C860.416 319.68 704.448 163.584 512 163.584L512 163.584zM776 400.576l-316.8 316.8c-9.728 9.728-25.472 9.728-35.2 0l-176-176c-9.728-9.728-9.728-25.472 0-35.2l35.2-35.2c9.728-9.728 25.472-9.728 35.2 0L441.6 594.176l264-264c9.728-9.728 25.472-9.728 35.2 0l35.2 35.2C785.728 375.104 785.728 390.848 776 400.576L776 400.576z"
                  p-id="1677"
                  fill="#a1d5a5"
                ></path>
              </svg>
              <span> 上传成功 </span>
            </div>
          </div>
          <div class="deleteUploadFiles">
            <svg
              t="1714033281397"
              class="icon"
              viewBox="0 0 1024 1024"
              version="1.1"
              xmlns="http://www.w3.org/2000/svg"
              p-id="5195"
              width="15"
              height="15"
            >
              <path
                d="M565.6 516.4l180.8-180.8c12.6-12.6 12.6-33 0-45.6-12.6-12.6-33-12.6-45.6 0L520.1 470.8 339.3 290.1c-12.6-12.6-33-12.6-45.6 0s-12.6 33 0 45.6l180.8 180.8-180.7 180.7c-12.6 12.6-12.6 33 0 45.6 12.6 12.6 33 12.6 45.6 0L520.2 562 701 742.8c12.6 12.6 33 12.6 45.6 0 12.6-12.6 12.6-33 0-45.6l-181-180.8z"
                fill="#bfbfbf"
                p-id="5196"
              ></path>
            </svg>
          </div>
        </div>
      </div>
      <div class="onceUploadFiles uploadFilesListBtn" v-loading="loading">
        <div class="uploadFilesListBtnCenter">
          <div class="uploadFilesListBtnMain">
            <svg
              t="1714034077479"
              class="icon"
              viewBox="0 0 1024 1024"
              version="1.1"
              xmlns="http://www.w3.org/2000/svg"
              p-id="6334"
              width="15"
              height="15"
            >
              <path
                d="M930.133 465.067H554.667V89.6c0-19.115-15.019-34.133-34.134-34.133S486.4 70.485 486.4 89.6v375.467H110.933c-19.114 0-34.133 15.018-34.133 34.133s15.019 34.133 34.133 34.133H486.4V908.8c0 19.115 15.019 34.133 34.133 34.133s34.134-15.018 34.134-34.133V533.333h375.466c19.115 0 34.134-15.018 34.134-34.133s-15.019-34.133-34.134-34.133z"
                p-id="6335"
                fill="#bfbfbf"
              ></path>
            </svg>
            <input type="file" @change="uploadFileViedo" />
            <p>添加视频</p>
          </div>
        </div>
      </div>
    </div>
    <div class="progressbarViedo"></div>
    <uploadFillinVue
      :hasFilesUploaded="hasFilesUploaded"
      :nowtimeUploadFile="nowtimeUploadFile"
    />
  </div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch, toRefs } from "vue";
import { PromisePool } from "./promisePool";
import { ElMessage } from "element-plus";
import uploadFillinVue from "./uploadFillin.vue";
import { MP4Clip } from "@webav/av-cliper";
import { getHash, spliceViedo } from "./upload";
import {
  uploadFileonce,
  mergerFiles,
  getrestStarIndexArr,
} from "@/views/client/api/uploadFile/uploadFile";
const uploadcount = ref(0);
const hasUploaded = ref<boolean>(false);
const fileUpload = ref(null);
const loading = ref(false);
let hasFilesUploaded = reactive<
  {
    name: any;
    [key: number]: any;
  }[]
>([]);
const nowtimeUploadFile = ref(0);
let restFilesIndexarr = reactive([]);
watch(fileUpload, (newvalue, oldvalue) => {
  if (newvalue) {
    hasUploaded.value = true;
  } else {
    hasUploaded.value = false;
  }
});
const uploadFileViedo = async (event: any) => {
  
  if (event.target.files[0]) {
    if (event.target.files[0].type != "video/mp4") {
      ElMessage.error("请您上传视频文件");
      return;
    } else {
      const files = event.target.files[0];
      let maxSize = 1024 * 1024 * 1;
      const { hashArr, suffix } = await getHash(files);
      let count = Math.ceil(files.size / maxSize);
      //有多少需要上传的文件
      if (count > 100) {
        count = 100;
        maxSize = files.size / count;
      }
      const chunk = await spliceViedo(count, files, maxSize, hashArr, suffix);

      const restArr = await getrestStarIndexArr(hashArr);
      const restFilesIndexarr = restArr.data;

      //  for(let i=0;i<chunk.length;i++){
      //   const fm = new FormData()
      //   fm.append('file',chunk[i].file)
      //   uploadFileonce(fm,i,hashArr,count).then((data:any)=>{
      //     if(data.code==20000){
      //       uploadFlagMethods(chunk.length,files.name,hashArr,files);
      //     }else{
      //       ElMessage.error('上传失败请重试')
      //       fileUpload.value=null
      //     }
      //   })
      //  }
      console.log(restFilesIndexarr);

      const asyncFunctions = chunk.map((file, index) => {
        return async () => {
          const fm = new FormData();
          fm.append("file", chunk[index].file);
          if (
            restFilesIndexarr &&
            restFilesIndexarr.length != 0 &&
            restFilesIndexarr !== null
          ) {
            if (restFilesIndexarr.includes(index)) {
              return uploadFileonce(fm, index, hashArr, count);
            }
          } else {
            return uploadFileonce(fm, index, hashArr, count);
          }
        };
      });

      // // 创建PromisePool实例
      const pool = new PromisePool(asyncFunctions, 5);

      // // 执行上传任务
      try {
        const results = await pool.exec();
        results.forEach((result: any) => {
          if (result.code == !20000) {
            ElMessage.error("您上传的文件没有成功,请重试");
            return;
          }
        });
        uploadFlagMethods(files.name, hashArr, files);
        // 处理上传结果
      } catch (error) {
        // 处理执行过程中的错误
      }
      loading.value = true;
      fileUpload.value = event.target.files[0];
    }
  }
};
//判断上传是否完成
const uploadFlagMethods = (name: any, md5: any, files: any) => {
  // 完成 合并
  mergerFiles(name, md5)
    .then(async (data: any) => {
      if (data.code === 20000) {
        ElMessage.success("文件上传成功");
        loading.value = false;
        //截取视频帧数
        const list = await splicePhotoList(files);
        const obj = {
          md5: md5,
          url: data.data,
          name: name,
          list: list,
        };
        hasFilesUploaded.push(obj);
      } else {
        ElMessage.error("文件上传失败");
        loading.value = false;
      }
      uploadcount.value = 0;
    })
    .catch((err) => {
      console.log(err);
    });
};
const splicePhotoList = async (file: any) => {
  const blob = new Blob([file], { type: file.type }); // 创建 Blob 对象
  const url = URL.createObjectURL(blob); // 创建本地文件的 URL
  const response: any = await fetch(url); // 使用 fetch 获取本地文件内容
  const clip = new MP4Clip(response.body);
  await clip.ready;

  const imgListData = await clip.thumbnails(200, {
    start: 0,
    end: file.lastModified,
    step: 1e6,
  });
  const imgaeListReturn = imgListData.map(
    (it: { ts: any; img: Blob | MediaSource }) => ({
      ts: it.ts,
      img: URL.createObjectURL(it.img),
    })
  );
  return imgaeListReturn;
};
</script>

<style>
@import "@/views/client/styles/viedoUpload/viedoCreate.scss";
@import "@/views/client/styles/viedoUpload/viedoUpload.scss";
@import "@/styles/component/component.scss";
.demo-progress .el-progress--line {
  margin-bottom: 15px;
  max-width: 600px;
}
.changePhoto:hover {
  cursor: pointer;
}
.fillInPhotoShow {
  border: 1px dashed var(--el-color-primary);
}
</style>

总结

分片上传的优势是很多的 在网上看到的学习到的代码对我也很有受益,下周的主要任务是实现弹幕的封装

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值