公司目前有个需求,就是老师上课的录像需要通过手机端小程序上传到服务器,而手机拍摄的视频一般会很大,虽然微信会自动压缩视频,但是难免的,视频依然会很大~~
微信自带的文件上传工具,虽然能上传大文件,但是。。。难免可能会出现网络波动等问题,导致文件上传失败,而且服务端也做了限制,单个文件不能超过20M,那么~问题来了,录播课程一节课一般都在200-300m左右,如何上传呢??
此时就需要用到大文件切片上传工具啦。我实现的思路很简单:
chooseVideo() {
uni.chooseVideo({
success: res => {
const uploadFile = new BigUpload({
url: `这是一个文件上传的路径`,
filePath: res.tempFilePath,
type: 'video/mp4',
byteLength: res.size,
size: 2097152,
fileName: 'weixin_video.mp4',
drowSpeed: (p) => {this.percent = p},
callback: (state) => {
if (state) {
this.percent = 100
this.uploadStatus = '上传完成'
this.videoMd5 = state.md5
}
}
})
uploadFile.startUpload()
}
})
}
2.文件选择成功后,读取文件基础信息,组装握手信息:
在chooseVideo选中文件后,tempFilePath就是文件的临时路径,res.size就是文件的大小总长度,剩余的参数就需要我们自行配置,例如type、size(分片大小)、fileName(文件名称,由于这个chooseVideo不能读取文件名,所以这里就自定义一个)等,配置如下:
url: `这是一个文件上传的路径`,
filePath: res.tempFilePath,
type: 'video/mp4',
byteLength: res.size,
size: 2097152,
fileName: 'weixin_video.mp4'
然后获取组装信息:
startUpload() {
this.chunkSize = this.Setting.size
if (!this.Setting.filePath) {
return
}
this.pt_md5 = ''
this.chunks = Math.ceil(this.Setting.byteLength / this.chunkSize)
this.currentChunk = 0
}
上传握手信息:
handshake(cbk, e) {
let formData = {}
let md5 = this.getDataMd5(e)
this.pt_md5 = md5
formData.pt_md5 = this.pt_md5
formData.chunks = this.chunks
formData.size = this.Setting.byteLength
formData.type = 'handshake'
formData.md5 = md5
formData.fileName = this.Setting.fileName
formData.contentType = this.Setting.type
postConsole({
url: this.Setting.url,
data: formData
}).then(res => {
if (res === 'success') {
cbk(true)
} else if (typeof res !== 'number') {
this.Setting.callback(res)
} else {
this.currentChunk = res
if (this.currentChunk < this.chunks) {
this.loadNext()
} else {
this.currentChunk--
this.loadNext()
}
}
}).catch(err => {
console.error(err)
cbk(false)
})
}
3.文件切割上传(最核心的来了):
a.先计算当前上传块的起始位置,以及计算上传进度:
loadNext() {
const p = this.currentChunk * 100 / this.chunks
this.drowSpeed(parseInt(p));
let start = this.currentChunk * this.chunkSize
let length = start + this.chunkSize >= this.Setting.byteLength ? this.Setting.byteLength - start : this.chunkSize
if (this.gowith) {
this.fileSlice(start, length, file => {
this.uploadFileBinary(file)
})
}
}
b.切片:
fileSlice(start, length, cbk) {
uni.getFileSystemManager().readFile({
filePath: this.Setting.filePath,
encoding: 'binary',
position: start,
length: length,
success: res => {
cbk(res.data)
},
fail: err => {
console.error(err)
this.callback(false)
}
})
}
c.上传,上传的逻辑是先根据切出来的文件块创建一个临时文件,然后上传这个临时文件,上传成功后就删除这个临时文件${wx.env.USER_DATA_PATH} 这里是用户数据目录,在uniapp中也必须这么写,不然无法识别路径:
uploadFileBinary(data) {
//获取文件系统句柄
const fs = uni.getFileSystemManager()
//计算数据md5
const md5 = this.getDataMd5(data)
//创建临时文件
const tempPath = `${wx.env.USER_DATA_PATH}/up_temp/${md5}.temp`
//授权创建
fs.access({
path: `${wx.env.USER_DATA_PATH}/up_temp`,
fail(res) {
fs.mkdirSync(`${wx.env.USER_DATA_PATH}/up_temp`, false)
}
})
//写入文件系统
fs.writeFile({
filePath: tempPath,
encoding: 'binary',
data: data,
success: res => {
let formData = {}
formData.currentChunk = this.currentChunk + 1
formData.pt_md5 = this.pt_md5
formData.type = 'file'
formData.md5 = md5
//上传文件片
uni.uploadFile({
url: this.Setting.url,
filePath: tempPath,
name: 'file',
formData: formData,
success: res2 => {
fs.unlinkSync(tempPath)
if (res2.statusCode === 200) {
const data = JSON.parse(res2.data)
if (data.code === '0') {
this.currentChunk++
//判断是否所有篇都上传了
if (this.currentChunk < this.chunks) {
//继续上传下一片
this.loadNext()
} else {
this.callback(data.data)
}
return true
}
}
//上传错误
this.callback(false)
},
fail: err => {
console.log(err)
this.callback(false)
}
})
},
fail: err => {
console.log(err)
this.callback(false)
}
})
}
4.文件合并:文件合并的操作主要在后端实现,实现逻辑也很简单,就是按照顺序将所有的文件块拼接起来就可以了。
5.上传成功:回显文件上传信息,比如路径、MD5等信息;
const uploadFile = new BigUpload({
url: `一个路径`,
filePath: res.tempFilePath,
type: 'video/mp4',
byteLength: res.size,
size: 2097152,
fileName: 'weixin_video.mp4',
drowSpeed: (p) => {this.percent = p},
callback: (state) => {
if (state) {
this.percent = 100
this.uploadStatus = '上传完成'
this.videoMd5 = state.md5
}
}
})
当callback失败时,返回false,当上传成功时,返回文件的信息。drowSpeed为绘制上传进度百分比。
大文件切片上传,最复杂的莫过于切片和上传这一块,之前研究uniapp文档时,上面写得很不详细,然后跑去微信官方文档上去查,微信文档上描述的比较清楚,我把地址贴出来戳这里FileSystemManager,有兴趣的可以看看.
后端以md5值为key,将进度存入redis,所以就算上传到一半有一个片失败了,那么下次重新上传时,会根据MD5值查询上次的上传进度,然后续传。当然也支持其他客户端上传,比如在上机上上传了10%,那么剩下的90%可以在电脑上继续上传,暂时不支持多客户端并行上传同一个文件。
upload.js
import SparkMD5 from 'spark-md5'
export const postConsole = (options) => {
let header = {...options.header}
return new Promise((resolve, reject) => {
uni.request({
url: options.url + '/console',
method: options.method || 'POST',
data: options.data || {},
dataType: 'json',
header,
success: (res) => {
if (res.data) {
if (res.data.code === '0') {
resolve(res.data.data)
} else {
reject(res.data.msg)
}
}
},
fail: (err) => {
reject(err)
}
})
})
}
export default class BigUpload {
constructor(Setting) {
this.Setting = Setting
}
startUpload() {
this.chunkSize = this.Setting.size
if (!this.Setting.filePath) {
return
}
this.pt_md5 = ''
this.chunks = Math.ceil(this.Setting.byteLength / this.chunkSize)
this.currentChunk = 0
this.gowith = true
this.fileSlice(0, this.Setting.byteLength, file => {
this.handshake(flag => {
if (flag) {
this.loadNext()
} else {
this.Setting.callback(false)
}
}, file)
})
}
handshake(cbk, e) {
let formData = {}
let md5 = this.getDataMd5(e)
this.pt_md5 = md5
formData.pt_md5 = this.pt_md5
formData.chunks = this.chunks
formData.size = this.Setting.byteLength
formData.type = 'handshake'
formData.md5 = md5
formData.fileName = this.Setting.fileName
formData.contentType = this.Setting.type
postConsole({
url: this.Setting.url,
data: formData
}).then(res => {
if (res === 'success') {
cbk(true)
} else if (typeof res !== 'number') {
this.Setting.callback(res)
} else {
this.currentChunk = res
if (this.currentChunk < this.chunks) {
this.loadNext()
} else {
this.currentChunk--
this.loadNext()
}
}
}).catch(err => {
console.error(err)
cbk(false)
})
}
loadNext() {
const p = this.currentChunk * 100 / this.chunks
this.drowSpeed(parseInt(p));
let start = this.currentChunk * this.chunkSize
let length = start + this.chunkSize >= this.Setting.byteLength ? this.Setting.byteLength - start : this.chunkSize
if (this.gowith) {
this.fileSlice(start, length, file => {
this.uploadFileBinary(file)
})
}
}
uploadFileBinary(data) {
const fs = uni.getFileSystemManager()
const md5 = this.getDataMd5(data)
const tempPath = `${wx.env.USER_DATA_PATH}/up_temp/${md5}.temp`
fs.access({
path: `${wx.env.USER_DATA_PATH}/up_temp`,
fail(res) {
fs.mkdirSync(`${wx.env.USER_DATA_PATH}/up_temp`, false)
}
})
fs.writeFile({
filePath: tempPath,
encoding: 'binary',
data: data,
success: res => {
let formData = {}
formData.currentChunk = this.currentChunk + 1
formData.pt_md5 = this.pt_md5
formData.type = 'file'
formData.md5 = md5
uni.uploadFile({
url: this.Setting.url,
filePath: tempPath,
name: 'file',
formData: formData,
success: res2 => {
fs.unlinkSync(tempPath)
if (res2.statusCode === 200) {
const data = JSON.parse(res2.data)
if (data.code === '0') {
this.currentChunk++
if (this.currentChunk < this.chunks) {
this.loadNext()
} else {
this.callback(data.data)
}
} else {
this.callback(false)
}
} else {
this.callback(false)
}
},
fail: err => {
console.log(err)
this.callback(false)
}
})
},
fail: err => {
console.log(err)
this.callback(false)
}
})
}
drowSpeed(p) {
if (this.Setting.drowSpeed != null && typeof (this.Setting.drowSpeed) === 'function') {
this.Setting.drowSpeed(p)
}
}
getDataMd5(data) {
if (data) {
let trunkSpark = new SparkMD5()
trunkSpark.appendBinary(data)
let md5 = trunkSpark.end()
return md5
}
}
isPlay(cbk) {
if (this.gowith) {
this.gowith = false
if (typeof (cbk) === 'function') cbk(false)
} else {
this.gowith = true
this.loadNext()
if (typeof (cbk) === 'function') cbk(true)
}
}
fileSlice(start, length, cbk) {
uni.getFileSystemManager().readFile({
filePath: this.Setting.filePath,
encoding: 'binary',
position: start,
length: length,
success: res => {
cbk(res.data)
},
fail: err => {
console.error(err)
this.callback(false)
}
})
}
callback(res) {
if (typeof (this.Setting.callback) === 'function') {
this.Setting.callback(res)
}
}
}