前端视频分片上传(blob)vue react uni 均适用

前言

开发过程中我们难免会遇到上传视频的需求。如果视频过大或者后端需要将前端上传的视频切割为播放更友好的m8u3格式,我们的分段上传视频就显得尤为重要 。或者后端不转m3u8也可以 直接文件合并也能达到效果


注意事项

下面的代码基于vue2.0框架使用混入的方法进行调用。需要注意的是如果你的项目使用了ESlint需要关闭while (true) 循环条件永恒进入的校验 or 用自己方法编写也是可以的 。 有任何疑问可联系作者询问,qq: 1274714546

方案二:纯js写法class类写法(推荐)

  • 封装class类

因后端处理方式统一 前端市面上没有太多的视频分片插件 此作品给大家留出了绝对的自用空间 适配大多数需求。 需要大家根据自己需求请求一下接口就好了。 其实任何文件不单视频都可以用这种方式进行分片上传
注意 !!下文中需要自己根据需求调接口的地方

import SparkMD5 from 'spark-md5'
// 此处使用了 spark-md5 来获取文件的md5值 不需要的可以删掉

class FileUploader {
  constructor(chunkSize = 1024 * 1024 * 2) {
    this.chunkSize = chunkSize; // 每片文件大小,默认为 2MB
    this.totalChunks = 0; // 文件总片数
    this.currentChunks = 0;// 当前上传片数
    this.progress = 0; // 文件上传进度
    this.progressT = null; // 更新文件上传进度定时器
  }

  // 上传文件
  async upload(file) {
    try {
      // 获取文件md5
      const spark = new SparkMD5()
      spark.append(file)
      const fileMD5 = spark.end()
      // 文件名字 xxx.mp4
      const fileName = file.name;

      // 文件分片 totalChunks片数
      const {chunkArr,totalChunks} = await this.sliceVideoFile(file, this.chunkSize)
      this.totalChunks = totalChunks;
      // 请求接口获取分片上传标识
      const {data:uploadId,fileKey} = await 请求接口获取文件标识({md5File:fileMD5,fileName:fileName});

      // 开始上传
      const arrPromise = await this.upChunkFile(chunkArr,uploadId,fileKey)
      
      // 所有内容上传成功
      await Promise.all(arrPromise);

      // 上传完成 - 组合文件
      await 请求接口组合文件()
      this.succeed(返回上传后文件地址);// 上传完成。根据业务逻辑返回相应内容
    } catch (error) {
      this.fail(error)
    }
  }

  /**
   * 上传每片文件
   * @param {Array} chunkArr 存放文件数组 FormData[]
   * @param {*} uploadId 上传所需参数
   * @param {*} fileKey  上传所需参数
   * @returns Promise 返回promise
   */
  upChunkFile(chunkArr,uploadId,fileKey){
    // 循环请求每一片( 并且成功更新上传进度) 
    // 最后返回promise数组
    return chunkArr.map((item,index)=>{
			// formdata参数根据后端接口调整
      item.append("fileKey",fileKey);
      item.append("uploadId",uploadId);
      return new Promise((resolve,reject)=>{
				// 请求接口上传美片文件
        请求接口(item).then(res=>{
          this.updateProgress();// 更新上传进度
          resolve()// 成功=> 具体返回参数根据业务逻辑来
        }).catch(err=>{
          reject(err)
        })
      })
    })

  }

  // 文件分片
  sliceVideoFile(file, chunkSize) {
    return new Promise((resolve, reject) => {
      const fileSize = file.size;
      let offset = 0;
      let chunkIndex = 0;
      const totalChunks = Math.ceil(fileSize / chunkSize); // 一共多少片
      let chunkArr = [];// 保存每片数组 

      const readNextChunk = () => {
        const blob = file.slice(offset, offset + chunkSize);
        const fileReader = new FileReader();
        // 文件加载成功
        fileReader.onload = (e) => {
          const chunkData = e.target.result;
          const chunk = {
            data: chunkData,
            index: chunkIndex,
          };
          offset += chunkSize;
          chunkIndex++;
  
          // 处理切片数据,例如上传到服务器等操作
          const formData1 = new FormData();
          formData1.append('partNumber', chunk.index + 1);// 第几片
          formData1.append('partFile', blob);// 文件
          chunkArr.push(formData1);
          
          if (offset < fileSize) {
            readNextChunk();
          } else {
            resolve({
              chunkArr, // 文件 FormData[]
              totalChunks // 总共多少片 mun
            });
          }
        };
        // 文件加载失败
        fileReader.onerror = (e) => {
          reject(e);
        };
        // 加载文件
        fileReader.readAsArrayBuffer(blob);
      };
      readNextChunk();
    });
  }


  // 缓慢增长到目标进度
  changeProgressBar(targetNum){
    cancelAnimationFrame(this.progressT);// 清除定时器
    const animationLoop = () =>{
      // 达到目标或者100 停止更新
      if(this.progress >= targetNum || this.progress >= 100){

      }else{
        this.progress++;
        this.onProgressUpdate(this.progress);// 同步更新上传进度
        this.progressT = requestAnimationFrame(animationLoop);// 继续请求下一帧动画
      }
    }
    this.progressT = requestAnimationFrame(animationLoop);
  }
  

  // 更新上传进度
  updateProgress(){
    this.currentChunks++;// 进度+1
    let percent = this.currentChunks / this.totalChunks * 100;
    this.changeProgressBar(percent);
  }

  // 上传完成
  succeed(res){
    this.reset();// 重置参数
    this.onUploadComplete(res);// 触发外部失败回调
  }

  // 上传失败
  fail(errTxt){
    this.reset();// 重置参数
    this.onUploadIncomplete(errTxt);// 触发外部失败失败回调
  }

  // 重置参数
  reset(){
    this.progress = 0;// 重置完成进度
    this.totalChunks = 0; // 文件总片数
    this.currentChunks = 0 ;// 当前上传片数
    cancelAnimationFrame(this.progressT);// 清除定时器
  }
	
}
  • 使用示例
// 使用示例
const fileUploader = new FileUploader(1024 * 1024 * 2);// 每片大小 默认2m 

// 自定义上传进度更新逻辑
fileUploader.onProgressUpdate = (progress) => {
 console.log(`当前上传进度:${progress}%`);
};
// 自定义上传完成逻辑
fileUploader.onUploadComplete = () => {
 console.log('文件上传已完成!');
};
// 自定义上传完成逻辑
fileUploader.onUploadIncomplete = () => {
 console.log('文件上传失败!');
};
// 上传对象
fileUploader.upload(file);

方案二:纯js写法objcet对象写法(推荐)

  • 封装对象

因后端处理方式统一 前端市面上没有太多的视频分片插件 此作品给大家留出了绝对的自用空间 适配大多数需求。 需要大家根据自己需求请求一下接口就好了。

const upVideo={
  file:null,//文件对象
  size:0,//分片大小 kb
  progressBar:0,//进度条 num%
  underWayFn:null,//进度条改变触发函数
  upOverFn:null,//上传完成触发函数
  t:null,//定时器
  // 初始化函数
  init({
    fileObj,//文件对象
    size=2,//分片大小 默认2  单位m
    underWayFn=function(){},//进度条改变触发函数
    upOverFn=function(){},//上传完成触发函数
  }){
    
    this.file=fileObj;
    this.size=size*1024*1024;
    this.underWayFn=underWayFn;
    this.upOverFn=upOverFn;
    // 判断是否是满足条件的视频对象 满足条件调用this.upbegin()处理
    this.upbegin();
  },
  // 上传文件
  async upbegin(){
    let videoTime = await this.videoLong();//视频播放时间
    
    // 视频分片
    let start = 0, end = 0, chunkArr= [], size = this.size;
    let file=this.file;
    function chuli(){
      end += size;
      var blob = file.slice(start, end);
      start += size;
      if (blob.size) {
        chunkArr.push(blob);
        chuli(file);
      }else{
        return chunkArr
      }
    }
    chuli();
	//预请求接口 然后this.inTurnto(chunkArr); 分片请求
    // 分片请求主体
    this.inTurnto(chunkArr);
        
    
    console.log(videoTime,this.file)
  },
  // 缓慢增长到目标进度条
  changeProgressBar(num){
    clearInterval(this.t)
    this.t = setInterval(() => {
      if(this.progressBar == num){
        clearInterval(this.t)
        //上传完成
        if(this.progressBar === 100){
          this.upOverFn();//通知上传完成
          this.clearUpVideo();//格式化重置
        } 
      }else{
        this.progressBar++;
        this.underWayFn(this.progressBar);//改变进度条通知
      }
    }, 50);
  },

  // 多个视频一一请求
  inTurnto(chunkArr){
    const chunkAllNum=chunkArr.length;//片段总数量
    let count=0;//完成个数
    chunkArr.forEach( (item,index) =>{
      // 模拟数据请求
      setTimeout(() => {
        console.log(item,index)
        count++;//增加当前进度
        this.changeProgressBar( parseInt(count/chunkAllNum*100) );//改变进度
      }, 1000);
    })
  },


  // 获取视频总时长
  videoLong(){
    return new Promise((resolve)=>{
      var url = URL.createObjectURL(this.file);
      var audioElement = new Audio(url);
      audioElement.addEventListener("loadedmetadata", function() {
        var hour = parseInt((audioElement.duration) / 3600);
        if (hour<10) { hour = "0" + hour; }
        var minute = parseInt((audioElement.duration % 3600) / 60);
        if (minute<10) {  minute = "0" + minute; }
        var second = Math.ceil(audioElement.duration % 60);
        if (second<10) { second = "0" + second; }
        resolve(hour + ":" + minute + ":" + second)
      });
    })
  },
  // 重置
  clearUpVideo(){
    this.file=null;
    this.size=0;
    this.progressBar=0;
    this.underWayFn=null;
    this.upOverFn=null;
    this.t=null;
  },
}


export default upVideo

  • 上文中需要自己根据需求调接口的地方

init 中条件判断是否是满足条件的视频对象进行处理
upbegin 中预请求接口
inTurnto中模拟数据的请求

  • 使用示例

这里没什么好说的 按照规则写就行

upVideo.init({
   fileObj:file,//视频文件 必传
   size:3,//分片大小  选填 默认2单位M
   underWayFn(num){//当前上传进度监听回调 选填
     console.log('进度',num)
   },
   upOverFn(){//上传完成生命周期 选填
     console.log('上传完成')
   },
 })

方案三:组件写法(不太推荐)

  • 组件代码
/*   使用示例
使用时更改自己的接口地址  使用到依赖axios
import upviedo from './upvideo.js'

mixins:[upviedo],

methods: {
  preview(file) {
    this.num++
    if(this.num>=2)return

    this.uploadViedo(上传文件对象,分片允许最大M(Num)).then(res=>{
      console.log('总体完成')
    }).catch(err=>{
      console.log('总体失败')
    })
  },
}

this.Loading   变量当前上传进度

*/
 

export default {
  data() {
    return {
      NowTotal:0,//当前
      AllTotal:1,//总数
      Loading:0,//当前进度%   
      clearT:0,//清除定时器
    };
  },
  mounted(){
    // 检测断网
    window.addEventListener("offline", () => {
      console.log("已断网");
    });
    window.addEventListener("online", () => {
      console.log("网络已连接");
    });
  },
  methods: {
    // 上传进度
    changeLoading(){
      this.NowTotal++;
      let ratio = (this.NowTotal * 100) / (this.AllTotal * 100)
      ratio = Math.round(ratio * 100)
      clearInterval(this.clearT)
      this.clearT = setInterval(()=>{
        this.Loading++;
        if(this.Loading>=ratio)clearInterval(this.clearT);
      },10)
    },
    // 视频长度
    videoLong(file){
      return new Promise((resolve,reject)=>{
        var url = URL.createObjectURL(file);
        var audioElement = new Audio(url);
        audioElement.addEventListener("loadedmetadata", function(_event) {
          var hour = parseInt((audioElement.duration) / 3600);
          if (hour<10) { hour = "0" + hour; }
          var minute = parseInt((audioElement.duration % 3600) / 60);
          if (minute<10) {  minute = "0" + minute; }
          var second = Math.ceil(audioElement.duration % 60);
          if (second<10) { second = "0" + second; }
          resolve(hour + ":" + minute + ":" + second)
        });
      })
    },
    // file :文件                      byte :每片字节大小 单位M
    async uploadViedo(file,byte = 2){
      
      var duration =await this.videoLong(file);

      let chunkSize = byte * 1024 * 1024 //分片大小
      let chunkArr = [];

      if (file.size < chunkSize) {
        chunkArr.push(file.slice(0))
      } else {
        var start = 0, end = 0
        while (true) {
          end += chunkSize
          var blob = file.slice(start, end)
          start += chunkSize

          if (!blob.size) {
            break;
          }
          chunkArr.push(blob)
        }
      }
      
      this.AllTotal = chunkArr.length;

   
      return new Promise((ok,no)=>{
        //预链接
        this.$axios.post('https:',{
          "video_number":this.AllTotal,//总共分为几片
          "size":file.size,//文件总大小
          "video_len": duration || 0,//视频长度
          "suffix":file.type,//文件类型
        }).then(res=>{
          let all=[];
          chunkArr.forEach((item,index)=>{
            let p = new Promise((resolve,reject)=>{
              var data = new FormData();
              data.append('video', item)//视频主体
              data.append('name', res.data.data)//视频名称
              data.append('blob_num', index+1)//当前为第几段视频
              data.append('total_blob_num', this.AllTotal)//总分为几段
              //上传主体
              this.$axios.post('https:',data).then(res=>{
                this.changeLoading();
                resolve(res)
              },err=>{
                reject(err)
              })
              
            })
            all.push(p);
          })

          Promise.all(all).then(res=>{
            ok() 
          }).catch(err=>{
            no(err)
          })

        }).catch(err=>{
          no(err)
        })

      })
     
    }
  },
  beforeDestroy(){
    clearInterval(this.clearT)
  }
}
  • 使用示例

这里仅用element组件 编写示例 方便理解cv到自己项目

<template>
  <div class="hello">


    <el-upload
      class="upload-demo"
      drag
      action="https://jsonplaceholder.typicode.com/posts/"
      :on-change="preview"
      :multiple="false">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
    </el-upload>



  </div>
</template>

<script>
import upviedo from './upvideo.js'
export default {
  mixins:[upviedo],
  props: {
    msg: String
  },
  created(){
  },
  data() {
    return {
      num:0,
    };
  },
  computed:{},
  mounted(){
  },
  methods: {
    preview(file) {
      this.num++
      if(this.num>=2)return
      console.log(file.raw)
		//上面代码都可以无视  这里是最主要的
		//第一个参数 :视频文件体   第二个参数:Num 每片视频限制最大字节(单位M) 不传默认2M
      this.uploadViedo(file.raw,2).then(res=>{
        console.log('总体完成')
      }).catch(err=>{
        console.log('总体失败')
      })
    },
  },
}
</script>

<style scoped>

</style>

总结

本文仅提供思路 根据自己的需求灵活修改 文中不对的地方欢迎大佬指正,使用中出现问题欢迎评论或私信
收藏方便下次使用 开源不易,绝对原创 点个赞吧

你可能感兴趣的:(vue,upload,上传视频,分片上传,文件分片上传)