一:前言(思路及问题)
1.uniapp中无法使用formdata,与我个人而言,很有影响
2.formdata无法使用,就只能通过uploadFile上传,这个有个formdata的参数,可以把formdata类的数据给到这个参数
3.然后说选择视频文件,选择视频文件uniapp给出了uni.chooseVideo来专门选择手机内的视频文件,这个api是支持app端的,参数之类的就不说了,但是就得到的数据来说,有tempFilePath选取文件的临时路径(app端支持)和temFile选取的文件file(只支持h5),这样一来对于要实现的功能来说,就有很大的问题,因为拿不到文件的file值,那就只能通过得到的临时文件地址去获取文件的file值
4.想通过这个临时地址获取文件的file值有点麻烦,uniapp相关的api但凡是能得到file值的都是只支持h5或小程序的,app端行不通,这样的就通过h5的api来实现了
5.plus.io.resolveLocalFileSystemURL通过URL参数获取目录对象或文件对象,这样就可以得到选择视频文件的file了
6.得到文件file后,说上传问题,uni.uploadFile参数file(仅支持h5)、filePath(要上传文件资源的路径)和name(文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容),其他的参数不说,就这三个参数而言,那能用的就filePath和name,视频文件切片后file数据中path都是原文件file的path值,路径是一样的,上传之后,都是整个的上传,不是切片后的文件
7.这样就iu意味着,如果文件切片后就这样去上传,实际的情况就是文件整个的上传了,没有达到切片上传的效果
8.我不知道有没有的其他的方式可以实现还是我的方法错了,反正我目前这样的方法是行不通的,所以我选择不用uni.uploadFile这个api了,这就又回到h5的api了
9.new一个plus.io.FileReader,然后read.readAsDataURL(file) ,这样是以URL编码格式读取文件数据内容,得到一个base64格式的file数据,将这样的file数据上传给后端后,一翻处理后,理论上效果是能实现了,但是....
10.nodejs接收到base64格式数据后,正常应该是将数据写入到文件,写入格式为binary,然后等所有请求处理后合并文件,最终达成效果
11. 但是实际的情况是,我将前端传来的base64格式数据转为buffer数据然后依次写入文件后,并接到合并请求后合并文件,最终的情况是合并文件打不开。(这个时候我的情况是:前端设定切片大小为2M,视频实际大小是1M,就不用切片了,这样的话nodejs接收base64格式的file数据处理后,最终的文件可以打开,但是文件只要大了,就会切片,最后的文件就打不开了)
12.这种情况,我有想是不是切片或者上传途中丢失了字符,不然怎么会文件小就行,文件大了就不行?但是我一直没有找到文件
13.这个时候我做了点小改变,之前的方法是接收切片的时候,每次接收都将切片数据先转buffer,然后写入文件,合并文件就是创建可写流和可读流来合并追加内容,这个时候我是在接收切片的时候将base64数据写入到txt文件,直接不转buffer,等到合并的时候,读取这几个切片文件得到里面的把base64,拼接这些字符,最后转buffer,写入,然后大功告成
二:uniapp
1.选取视频文件
// 打开选择文件
await uni.chooseVideo({
count:1,
mediaType:['video'],
sourceType:['album'],
success: async(res) => {
plus.io.resolveLocalFileSystemURL(res.tempFilePath,(entry) => {
entry.file(async (ent) => {
this.file = ent
// uploadVideo是我从其他的js文件导入的方法
await uplaodVideo(ent)
})
})
}
})
2.开始切片
function sliceVideo (file,fileMd5) {
return new Promise((res,rej) => {
try {
// 当前已切片数
let sliceNum = 0;
// 切片文件数组
let sliceVideoArr = [];
console.log('file',file);
// new一个plus.io.FileReader
let read = new plus.io.FileReader()
// read.readAsDataURL(file)以URL编码格式读取文件数据内容
read.readAsDataURL(file)
// 文件读取操作完成时的回调函数 得到一个base64格式的数据
read.onloadend = (e) => {
// blob = e.target.result.replace(/^data:video\/\w+;base64,/, '');
// 这里是因为现在得到的字符数据前面有些没有用的东西,切割一下
let blob = read.result.split(',')[1]
// 应切片总数
sliceAllNum = Math.ceil(blob.length / sliceSize)
let start = 0;
let end = 0;
let fileStr;
// 这里来一个递归函数,一次次切割,直到最后一次
const recursition = () => {
console.log('sliceNum',sliceNum);
start = sliceNum * sliceSize
end = Math.min(blob.length, start + sliceSize)
fileStr = blob.slice(start,end)
sliceVideoArr.push({index:sliceNum,file:fileStr,md5:fileMd5})
sliceNum++
if(sliceNum < sliceAllNum) {
recursition()
} else {
res(sliceVideoArr)
}
}
recursition()
}
} catch(err) {
console.log('try-catch-err',err);
}
})
}
3.上传(uniapp全部代码)
import { request,url } from './request.js'
// 用来获取文件的唯一标识 yarn add spark-md5 --save
import sparkMd5 from 'spark-md5'
// 规定每片的大小 byte->kb->mb 限制每片大小为2mb
let sliceSize = 1024 * 1024 * 2;//1024 * 1024 * 2
// 应切片总数
let sliceAllNum = 0;
// 当前上传进度
let nowprogress = 0;
export const uplaodVideo = async(file) => {
// 获取文件得唯一值
let spark = new sparkMd5.ArrayBuffer()
spark.append(file)
let fileMd5 = spark.end()
// 开始切片
let sliceVideoArr = await sliceVideo(file,fileMd5)
console.log('sliceVideoArr',sliceVideoArr);
// 上传
let upload = uploadVideo(sliceVideoArr)
await Promise.all(upload)
console.log('promise_all');
const full = await request({url:`/issue/full_video?md5=${fileMd5}`,method:'get'})
console.log('full',full);
}
function sliceVideo (file,fileMd5) {
return new Promise((res,rej) => {
try {
// 当前已切片数
let sliceNum = 0;
// 切片文件数组
let sliceVideoArr = [];
console.log('file',file);
let read = new plus.io.FileReader()
read.readAsDataURL(file)
read.onloadend = (e) => {
// blob = e.target.result.replace(/^data:video\/\w+;base64,/, '');
let blob = read.result.split(',')[1]
// 应切片总数
sliceAllNum = Math.ceil(blob.length / sliceSize)
let start = 0;
let end = 0;
let fileStr;
const recursition = () => {
console.log('sliceNum',sliceNum);
start = sliceNum * sliceSize
end = Math.min(blob.length, start + sliceSize)
fileStr = blob.slice(start,end)
sliceVideoArr.push({index:sliceNum,file:fileStr,md5:fileMd5})
sliceNum++
if(sliceNum < sliceAllNum) {
recursition()
} else {
res(sliceVideoArr)
}
}
recursition()
}
} catch(err) {
console.log('try-catch-err',err);
}
})
}
function uploadVideo(sliceVideoArr) {
return sliceVideoArr.map((val,ins) => {
console.log('map-val',val);
return new Promise((res,rej) => {
uni.request({
url:`${url}/issue/slice_video`,
method:'post',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data:val,
success: (req) => {
res(req)
},
fail: (err) => {
rej(err)
}
})
})
})
}
三:nodejs
1.准备工作
// 中间件处理
// 因为现在选择的实现方式不是直接上传file,而是base64数据,字符会很长,所以需要针对中间件进行处理,让其可接收的字符长度变得更大
app.use(bodyParse.json({limit:'300mb',extended: true}));
2.接收切片文件
// 接收切片的视频,写入指定文件
router.post('/slice_video',(req,res) => {
try {
console.log('req.body',req.body.index);
let paths = path.join(__dirname,`../sliceVideo/${req.body.index}.txt`)
// 创建可写流
const writeStream = fs.createWriteStream(paths);
// 将base64数据通过流写入文件
writeStream.write(req.body.file, (err) => {
if (err) {
console.error('写入文件错误', err);
} else {
console.log('文件写入成功');
// 关闭可写流
// writeStream.end();
}
});
res.send({code:200})
} catch (err) {
console.log('slice_video_err',err);
}
})
3.合并文件
// 合并切片
router.get('/full_video',async (req,res) => {
try {
// , { encoding: 'binary' }
let slicePath = path.join(__dirname,'../sliceVideo')
let fullPath = path.join(__dirname,'../fullVideo/all.mp4')
fs.readdir(slicePath,async(err,files) => {
if(err) {
console.log('fs.readdir--err',err);
return;
}
let arr = files
arr.sort((a,b) => {
return a.slice('.')[0]-b.slice('.')[0]
});
console.log('arr--------------',arr);
let str = '';
// 创建可写流
for(var i = 0;i < arr.length;i++) {
console.log('index',i);
let data = await readFile(path.join(slicePath,arr[i]))
str+=data
}
const binaryData = Buffer.from(str, 'base64')
const writeStream = fs.createWriteStream(fullPath);
writeStream.write(binaryData, 'binary', (err) => {
if (err) {
console.error('写入文件错误', err);
} else {
console.log('文件写入成功');
// 关闭可写流
writeStream.end();
}
});
function readFile(videopath){
return new Promise((res,rej) => {
fs.readFile(videopath,'utf8',(readerr,readdata) => {
if(readerr) {
console.log('fs.readFile---readerr',readerr);
return;
}
console.log('readdata',readdata);
res(readdata)
})
})
}
})
} catch(err) {
console.log('err----errr',err);
}
})
至此,这个功能是已经实现了
只是还欠些需要完善的地方,比如在sliceVideo文件中,需要以文件名再建一个文件,不同的切片文件保存到各自的文件夹中
然后在合并中,合并完成后,把这个合并的切片文件和文件夹删除掉
断点续传,每一次上传切片做好记录,这个就以自己的方式做些数据来记录,当前第一次上传被中断后,第二次上传请求发起后,nodejs就查询之前自己留的数据看是已经上传到那了,把查到的数据返给前端,前端就在那个地方继续上传(然后因为我这个方法获取的文件数据一开始是文件的临时路径,我不清楚这个文件的临时路径是否会变?第二次不用api选取直接通过之前的临时文件地址是否能行?这些都不是问题,试一下然后折中处理就ok了)
记录一下,接收数据小视频的其他方法
方法一:multer中间件
const express = require('express')
const router = express.Router()
const path = require('path')
const fs = require('fs');
const multer = require('multer')
let storage = multer.diskStorage({
destination: function(req, file, cb) {
// console.log('req',req.body);
cb(null, 'sliceVideo/');
},
filename: function(req, file, cb) {
console.log('multer-req',req.body,req.params,req.query);
console.log('multer-file',file);
let filename = file.originalname + '.' + `${req.body.index}` + '.mp4'
cb(null, filename)
}
})
// 记得diskStorage用完后,放到multer里面来
let upload = multer({ storage: storage });
// upload.single是针对只上串一个视频文件 upload.array是多个
// 后面的这个video是前端在上传文件时使用formData append命名,如formdata.append('video',file),前端和后端的这个命名要一致
// 到这里前端上传的视频已经是保存到指定文件中了
router.post('/slice_video',upload.single('video'),(req,res) => {
// 这里现在就做些其他的处理就行
})
方法二:formidable(这个就不用像multer一样需要设置video之类的命名了,但是有一个问题,用这个的话,请求数据req就是空的了,需要通过form.parse才能得到req的数据)
router.post('/slice_video',(req,res) => {
console.log('body',req.body);
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.uploadDir = path.join(__dirname,'../videoUpload')
form.parse(req,(err,fields,files) => {
console.log('body',req.body); fs.rename(path.join(__dirname,`../videoUpload/${files.videofile[0].newFilename}`),path.join(__dirname,`../videoUpload/${files.videofile[0].newFilename + '.mp4'}`),(err) => {
console.log('fs-renma',err);
})
})
})