这几天接到一个需求是这样的,用户需要在客户端上传视频,一般大小都在50M以上。
最开始我们的方案是先把文件上传到后端,后端再上传到阿里云OSS的。
由于文件过大,文件上传非常慢。为了用户体验增加了等待进度条,但这时又出现了新的问题,进度条100%但是后端没及时成功响应。因为后端虽然接收了文件,但阿里云OSS还没有返回响应,这个耗时也是非常久。于是考虑使用前端直传阿里云OSS。
oss配置
首先我们需要对OSS进行以下的配置:
如果开发小程序的话需要配置uploadFile合法域名(oss的域名)。
uniapp代码
uniapp端的话可以考虑使用这个插件。
阿里云oss文件直传-无需后台签名 - DCloud 插件市场
插件修改
修改了插件内的部分代码
uploadFile.js
const env = require('./config.js'); //配置文件,在这文件里配置你的OSS keyId和KeySecret,timeout:87600;
const base64 = require('./base64.js'); //Base64,hmac,sha1,crypto相关算法
require('./hmac.js');
require('./sha1.js');
const Crypto = require('./crypto.js');
const getPolicyBase64 = function() {
let date = new Date();
date.setHours(date.getHours() + env.timeout);
let srcT = date.toISOString();
const policyText = {
"expiration": srcT, //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了
"conditions": [
["content-length-range", 0, 5000 * 1024 * 1024] // 设置上传文件的大小限制,5mb
]
};
const policyBase64 = base64.encode(JSON.stringify(policyText));
console.log(policyBase64);
return policyBase64;
}
const getSignature = function(policyBase64) {
const accesskey = env.AccessKeySecret;
const bytes = Crypto.HMAC(Crypto.SHA1, policyBase64, accesskey, {
asBytes: true
});
const signature = Crypto.util.bytesToBase64(bytes);
console.log(signature);
return signature;
}
const aliyunServerURL = env.uploadImageUrl; //OSS地址,需要https
const accessid = env.OSSAccessKeyId;
const policyBase64 = getPolicyBase64();
const signature = getSignature(policyBase64); //获取签名
const uploadFile = {
url: aliyunServerURL, //开发者服务器 url
policyBase64,
accessid,
signature,
}
module.exports = uploadFile;
使用
html代码
<view class="upVideo" :style="{paddingTop:titleHeight+'px'}" @click="chooseVideo()" v-if="video_type != 'link'">
<image class="upvideo" src="/static/upvideo.png" mode=""></image>
<image class="hasVido" :src="fromData.cover_image_url" mode="" v-if="fromData.cover_image_url"></image>
<view class="addBox loading-box" v-if="loading">
<view class="loading">
<u-circle-progress active-color="#333333" width="240" duration="100" :percent="percent">
<view class="u-progress-content">
<template v-if="percent==100">
<template v-if="state == 'fail'">
<view class="u-progress-dot fail"></view>
<text class='u-progress-info'>上传失败</text>
</template>
<template v-else>
<view class="u-progress-dot success"></view>
<text class='u-progress-info'>上传完成</text>
</template>
</template>
<template v-else>
<view class="u-progress-dot"></view>
<text class='u-progress-info'>上传{{percent}}%</text>
</template>
</view>
</u-circle-progress>
</view>
</view>
<view class="addBox" v-else>
<image class="jv" src="/static/jv.png" mode=""></image>
<view class="text">上传视频</view>
<!-- <view class="texts">MP4格式(手机拍摄),最大50M</view> -->
<view class="texts">MP4/3GP/M3U8格式(手机拍摄),时长30~120秒</view>
</view>
</view>
//引入插件
import uploadImage from '@/js_sdk/yushijie-ossutil/ossutil/uploadFile.js';
methods: {
chooseVideo() { // 上传视频
var that = this;
uni.chooseVideo({
count: 1,
compressed: false,
sourceType: ['album', 'camera'],
success: async function(resVideo) {
// let maxSize = (resVideo.size / 1024 / 1024).toFixed(2)
// if (maxSize > 50) {
// uni.showToast({
// icon: 'none',
// title: '视频最大为50M',
// })
// return false
// }
console.log(resVideo);
const extension = ["mp4", "3gp", "m3u8"]
console.log(resVideo);
let maxDuration = resVideo.duration
// #ifdef H5
const type = resVideo.tempFile.type.split('/')[1]
var ua = navigator.userAgent.toLowerCase();
var isWeixin = ua.indexOf('micromessenger') != -1;
if (isWeixin) {
// 微信浏览器内大视频获取duration经常性为0,特此进行处理
maxDuration = await that.getDuration(resVideo);
}
// #endif
// #ifdef MP-WEIXIN
const index = resVideo.tempFilePath.lastIndexOf('.')
const type = resVideo.tempFilePath.slice(index + 1)
// #endif
if (!extension.includes(type)) {
uni.showToast({
icon: 'none',
title: '视频格式不支持',
})
return false
}
if (maxDuration < 30) {
uni.showToast({
icon: 'none',
title: '视频必须大于30秒',
})
return false
}
if (maxDuration > 120) {
uni.showToast({
icon: 'none',
title: '视频最长为2分钟',
})
return false
}
const {
url,
policyBase64,
accessid,
signature
} = uploadImage
that.loading = true
that.state = ""
that.percent = 0
that.fromData.video_url = ""
that.fromData.cover_image_url = ""
const aliyunFileKey = 'video/' + new Date().getTime() + Math.floor(Math.random() *
150) + '.' + type;
let uploadTask = uni.uploadFile({
url, //仅为示例,非真实的接口地址
filePath: resVideo.tempFilePath,
formData: {
'key': aliyunFileKey,
'policy': policyBase64,
'OSSAccessKeyId': accessid,
'signature': signature,
'success_action_status': '200',
},
name: 'file',
success: (uploadFileRes) => {
if (uploadFileRes.statusCode == 200) {
uni.showToast({
icon: 'none',
title: "上传成功"
})
console.log(url + aliyunFileKey);
that.fromData.video_url = url + aliyunFileKey
that.fromData.cover_image_url = url + aliyunFileKey +
"?x-oss-process=video/snapshot,t_1000,f_jpg,m_fast"
that.state = 'success'
setTimeout(() => {
that.percent = 0
that.loading = false
}, 3000)
} else {
that.state = 'fail'
setTimeout(() => {
that.percent = 0
that.loading = false
}, 3000)
uni.showToast({
icon: 'none',
title: '上传失败,请更换视频或重新上传'
})
}
},
fail: (err) => {
console.log(err);
that.state = "fail"
setTimeout(() => {
that.percent = 0
that.loading = false
}, 3000)
uni.showToast({
icon: 'none',
title: '上传失败,请更换视频或重新上传'
})
}
});
uploadTask.onProgressUpdate(that.onProgressUpdate((res) => {
// console.log('上传进度' + res.progress);
// console.log('已经上传的数据长度' + res.totalBytesSent);
// console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
that.percent = res.progress
}, 100))
}
})
},
onProgressUpdate(func, wait = 50) {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
},
}
H5端的话要考虑AccessKeySecret
以及OSSAccessKeyId
暴露的问题,可以考虑单独用ali-oss.js库单独进行处理,或者做好OSS的域名设置防止跨域请求。
PC端
PC端使用了ali-oss
库进行开发
插件修改
/**
* 阿里云oss上传工具
*/
let OSS = require('ali-oss');
let config = {
region: 'oss-cn-hangzhou',
secure:true,
accessKeyId: '',
accessKeySecret: '',
bucket: ''
};
/**
* 配置
*/
let init = ()=>{
return new OSS(config);
}
/**
* 生成uuid
*/
let guid = ()=>{
let S4 = ()=>{
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}
/**
* 修改文件名字
*/
let fileName = (file)=>{
let arr = file.name.split(".");
var uuid = "oss"+guid();
if(arr.length>1){
return uuid+'.'+arr[arr.length-1];
}else{
return uuid;
}
}
/**
* 上传文件
*/
let ossPut = (file,callback)=>{
return new Promise((resolve, reject) => {
let objectName = fileName(file);
//videos/指的是放在oss的videos目录下
init().multipartUpload('videos/'+objectName, file,{
progress(p){ // 获取上传进度,上传进度为0 - 1, 1为上传100%
callback(p)
}
}).then(({res,url}) => {
if (res && res.status == 200) {
console.log('阿里云OSS上传文件成功回调', res,url);
resolve(res,url);
}
}).catch((err) => {
console.log('阿里云OSS上传文件失败回调', err);
reject(err)
});
})
}
/**
* 下载文件
*/
let ossGet = (name)=>{
return new Promise((resolve, reject) => {
init().get(name).then(({res}) => {
if (res && res.status == 200) {
console.log('阿里云OSS下载文件成功回调', res);
resolve(res);
}
}).catch((err) => {
console.log('阿里云OSS下载文件失败回调', err);
reject(err)
});
})
}
export default {ossPut,ossGet}
使用
import ossClient from './api/aliyun.oss.client.js';
Vue.prototype.$ossClient = ossClient;
<el-upload
class="avatar-uploader"
accept=".mp4,.3gp,.m3u8"
:http-request="uploadHttp"
action
:show-file-list="false"
:before-upload="beforeUploadVideo"
>
<img v-if="postData.cover_image_url" :src="postData.cover_image_url" class="avatar">
<div v-else class="upIocn">
<div v-if="videoFlag == false">
<img class="add" src="../assets/add.png" alt="">
<div class="tip">上传视频</div>
<div class="msg">MP4格式(手机拍摄),最长2分钟,最短30秒</div>
</div>
</div>
<el-progress class="progress" v-if="videoFlag == true" type="circle" :percentage="parseFloat(videoUploadPercent)" />
</el-upload>
methods:{
beforeUploadVideo(file){
const that = this
return new Promise((resolve,reject)=>{
let url = URL.createObjectURL(file);
let audioElement = new Audio(url);
audioElement.addEventListener("loadedmetadata", (_event)=> {
console.log(_event);
// let maxSize = (file.size / 1024 / 1024).toFixed(2)
// if (maxSize > 50) {
// that.$message({
// type: 'error',
// message: '视频最大为50M',
// })
// reject(false)
// return
// }
let duration = audioElement.duration;
console.log('duration',duration);
// 大小 时长
if (duration < 30) {
//超过需求限制
that.$message({
message: '视频必须大于30秒',
type: 'error'
});
reject(false)
return
}
if (duration > 120) {
//超过需求限制
that.$message({
message: '视频最长为2分钟',
type: 'error'
});
reject(false)
return
}
resolve(true)
});
})
},
coverImageSuccess(e) {
this.postData.cover_image_url = e.data.cover_image_url
this.postData.video_url = e.data.video_url
this.videoFlag = false
},
progress(p){
let pecent = p * 100
this.videoFlag = true
this.videoUploadPercent = pecent.toFixed(0)
},
uploadHttp({ file }) {
this.postData.cover_image_url = ""
this.postData.video_url = ""
this.videoFlag = true
try{
this.$ossClient.ossPut(file, this.progress).then(res=>{
if(res.statusCode==200){
let video_url = res.requestUrls[0]
let index = video_url.lastIndexOf('?')
video_url = video_url.slice(0, index)
const cover_image_url = video_url + "?x-oss-process=video/snapshot,t_1000,f_jpg,m_fast"
const data = {
video_url,
cover_image_url
}
console.log('data');
this.coverImageSuccess({
data
})
}else{
this.$message.error('上传失败,请更换视频或重新上传')
}
})
}catch(e){
//TODO handle the exception
this.$message.error('上传失败,请更换视频或重新上传')
}
},
}