這幾天接到一個需求是這樣的,用戶需要在客戶端上傳視頻,一般大小都在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('上傳失敗,請更換視頻或重新上傳')
}
},
}