开发过程中我们难免会遇到上传视频的需求。如果视频过大或者后端需要将前端上传的视频切割为播放更友好的m8u3格式,我们的分段上传视频就显得尤为重要 。或者后端不转m3u8也可以 直接文件合并也能达到效果
下面的代码基于vue2.0框架使用混入的方法进行调用。需要注意的是如果你的项目使用了ESlint需要关闭while (true) 循环条件永恒进入的校验 or 用自己方法编写也是可以的 。 有任何疑问可联系作者询问,qq: 1274714546
因后端处理方式统一 前端市面上没有太多的视频分片插件 此作品给大家留出了绝对的自用空间 适配大多数需求。 需要大家根据自己需求请求一下接口就好了。 其实任何文件不单视频都可以用这种方式进行分片上传
注意 !!下文中需要自己根据需求调接口的地方
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);
因后端处理方式统一 前端市面上没有太多的视频分片插件 此作品给大家留出了绝对的自用空间 适配大多数需求。 需要大家根据自己需求请求一下接口就好了。
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>
本文仅提供思路 根据自己的需求灵活修改 文中不对的地方欢迎大佬指正,使用中出现问题欢迎评论或私信
收藏方便下次使用 开源不易,绝对原创 点个赞吧