1.基础视频拍摄+上传功能, 采用van-uploader+oss上传, 小文件采用前端直传oss普通上传, 大文件采用oss分片上传, oss官方文档: help.aliyun.com/document_de…
实现参考: blog.csdn.net/zmx729618/a…
<template>
<van-uploader
class="upload-auth"
:max-count="1"
accept="video/*"
v-model="fileList"
capture="camera"
:max-size="maxSize * 1024 * 1024"
:before-read="beforeUpload"
:after-read="handleSuccess"
@oversize="onOversize"
>
</van-uploader>
</template>
<script>
import { getALYInfo } from "@/http/ossUpload";
import { saveDetailView, updateDetailView } from "@/http/api-leads";
const OSS = require("ali-oss");
var randomstring = require("randomstring");
export default {
data() {
return {
file: null,
fileList: [],
fileUrl: '',
// 分片上传
ossInfo: {},
credentials : null, // STS凭证
ossClient : null, // oss客户端实例
bucket : 'xxxx-xxx-xxx', // bucket名称
region : 'xxx', // oss服务区域名称
partSize : 10 * 1024 * 1024, // 每个分片大小(byte),设置10M
parallel : 4, // 同时上传的分片数
checkpoints : {}, // 所有分片上传文件的检查点
progress: 0, // 上传进度
showProgress: false, // 是否展示进度条
selectToBack: false, // 选择返回
disabled: false,
maxSize: 1024, // 视频最大1G, 单位M
maxImgSize: 10, // 图片最大10M, 单位M
};
},
created() {
this.initOSSClient();
},
methods: {
// 返回重新选择
backToSelect() {
let video = this.$refs.video;
if(this.fileType === 1 && video) {
video.pause();
}
this.$emit('update:showUpload', false);
this.$emit('showModeSelect');
if(JSON.stringify(this.checkpoints) !== '{}') {
this.selectToBack = true;
this.checkpoints = {};
this.stop();
}
},
// 关闭上传预览弹窗
closeUploadDialog() {
let video = this.$refs.video;
if(this.fileType === 1 && video) {
video.pause();
}
this.$emit('update:showUpload', false);
if(JSON.stringify(this.checkpoints) !== '{}') {
this.selectToBack = true;
this.checkpoints = {};
this.stop();
}
},
// 上传文件
beforeUpload(file) {
this.file = file;
let isLtM = file.size / 1024 / 1024 < this.maxSize;
if (!isLtM) this.$toast.fail(`请上传${this.maxSize / 1024}G以内的视频`);
this.progress = 0;
this.fileUrl = '';
this.upload();
return isLtM;
},
// 视频超过1G
onOversize() {
this.$toast(`请上传${this.maxSize / 1024}G以内的视频`);
},
// * 上传成功的回调
handleSuccess(file) {
console.log('上传的文件:', file)
this.progress = 0;
this.fileUrl = file.content;
// this.fileUrl = window.URL.createObjectURL(file.file);
},
// 点击上传按钮事件
async upload() {
// 获取STS Token
// await this.getCredential();
console.log('上传的文件', this.file)
// 如果文件大小小于分片大小,使用普通上传,否则使用分片上传
if (this.file.size < this.partSize) {
this.uploadHandle();
} else {
console.log('超过1M分片上传')
if(JSON.stringify(this.checkpoints) === '{}') {
this.multipartUpload(this.file);
} else {
this.resume()
}
}
this.showProgress = true;
},
// 普通上传
async uploadHandle() {
console.log('普通上传:', this.file)
let {tenantId} = JSON.parse(localStorage.getItem('leadsUserInfo'));
let {name} = this.file;
let lastName = name.replace(/[~!@#$%^+ ]/g,"");
let fileUuid = randomstring.generate(24);
let fileName = fileUuid + '-' + lastName;
// console.log(fileName,this.file,lastName,'file文件......')
let timer = null;
try {
timer = setInterval(() => {
if (this.progress < 90) {
this.progress += Math.floor(Math.random()*5);
console.log('44444:', this.progress);
}
}, 500);
const result = await this.ossClient.put(`leads/${tenantId}/H5/${fileName}`, this.file);
clearInterval(timer);
this.progress = 100;
// this.fileName = result.name;
// this.fileUrl = result.url; // 上传成功的url
// 将上传的url传给父组件
// this.successCallBack(result.url);
} catch (e) {
console.log(e)
this.progress = 0;
clearInterval(timer);
this.errorCallBack();
}
},
// 分片相关配置
initOSSClient() {
let headers = {
authorizeToken: JSON.parse(
localStorage.getItem("leadsUserInfo")
).userToken,
tenantNo: JSON.parse(localStorage.getItem("leadsUserInfo"))
.tenantId,
};
// 获取oss信息
getALYInfo(headers).then(res => {
if (res.data.code == 200) {
let {data} = res.data;
this.credentials = data;
Object.assign(this.ossInfo, data);
// console.log(this.ossInfo);
this.ossClient = new OSS({
accessKeyId: data.accessKeyId,
accessKeySecret: data.accessKeySecret,
stsToken: data.securityToken,
endpoint: 'https://oss-cn-shanghai.aliyuncs.com',
refreshSTSTokenInterval: 1800000,
bucket: data.bucketName,
secure: true,
timeout: 5 * 60 * 1000,
refreshSTSToken: async () => {
this.initOSSClient();
}
});
}
})
},
// 分片上传
async multipartUpload(file) {
// if (!this.ossClient) {
// await this.initOSSClient();
// }
console.log('分片上传:')
// const fileName = file.name;
let {tenantId} = JSON.parse(localStorage.getItem('leadsUserInfo'));
let {name} = file;
let lastName = name.replace(/[~!@#$%^+ ]/g,"");
let fileUuid = randomstring.generate(24);
let fileName = fileUuid + '-' + lastName;
try {
return this.ossClient.multipartUpload(`leads/${tenantId}/H5${fileName}`, file, {
parallel: this.parallel,
partSize: this.partSize,
progress: this.onMultipartUploadProgress
}).then(result => {
// 生成文件下载地址
// const url = `http://${this.bucket}.${this.region}.aliyuncs.com/leads/${tenantId}/H5/${fileName}`;
let { requestUrls = [] } = result.res || {}
let requestUrl = requestUrls.length ? requestUrls[0] : '';
let url = requestUrl.indexOf("?") > -1 ? requestUrl.substring(0, requestUrl.indexOf("?")) : requestUrl;
console.log(`分片上传 ${file.name} 成功, url === `, url, result);
// this.successCallBack(url);
}).catch(err => {
console.log(`分片上传 ${file.name} 失败 === `, err);
this.errorCallBack();
});
} catch (error) {
this.errorCallBack();
}
},
// 分片上传进度改变回调
async onMultipartUploadProgress(progress, checkpoint) {
this.progress = (progress * 100).toFixed(0);
console.log(`${checkpoint.file.name} 上传进度 ${this.progress}`);
this.checkpoints[checkpoint.uploadId] = checkpoint;
// 判断STS Token是否将要过期,过期则重新获取
const { Expiration } = this.credentials;
const timegap = 1;
if (Expiration && this.moment(Expiration).subtract(timegap, 'minute').isBefore(this.moment())) {
console.log(`STS token will expire in ${timegap} minutes,uploading will pause and resume after getting new STS token`);
if (this.ossClient) {
this.ossClient.cancel();
}
await this.getCredential();
await this.resumeMultipartUpload();
}
},
// 上传成功
successCallBack(url = '') {
this.$toast.success("上传成功!");
this.file = null;
this.checkpoints = {};
this.showProgress = false;
this.disabled = false;
this.$emit('getUploadDetail')
this.$emit('update:showUpload', false);
},
// 上传失败
errorCallBack() {
if(!this.selectToBack) {
this.$toast.fail("上传失败,请重新上传!");
}
this.disabled = false;
this.showProgress = false;
},
// 断点续传
async resumeMultipartUpload() {
console.log('断点续传:', this.checkpoints);
Object.values(this.checkpoints).forEach((checkpoint) => {
const { uploadId, file, name } = checkpoint;
this.ossClient.multipartUpload(uploadId, file, {
parallel: this.parallel,
partSize: this.partSize,
progress: this.onMultipartUploadProgress,
checkpoint
}).then(result => {
console.log('before delete checkpoints === ', this.checkpoints, result);
// delete this.checkpoints[this.checkpoint.uploadId];
console.log('after delete checkpoints === ', this.checkpoints);
let { requestUrls = [] } = result.res || {}
let requestUrl = requestUrls.length ? requestUrls[0] : '';
let url = requestUrl.indexOf("?") > -1 ? requestUrl.substring(0, requestUrl.indexOf("?")) : requestUrl;
console.log(`Resume multipart upload ${file.name} succeeded, url === `, url, )
this.successCallBack(url);
}).catch(err => {
console.log('Resume multipart upload failed === ', err);
this.errorCallBack();
});
});
},
},
};
</script>
2.van-uploader拍摄视频/上传本地文件
capture="camera" 调起原生摄像头拍摄, 设置其他值可以选择本地文件和拍摄, 但是没有属性来限制只能选择本地文件。accept="video/"设置文件类型是视频, accept="image/"设置文件类型为图片。
3.视频/图片预览
采用原生video标签实现视频预览功能(videoImgSrc是默认封面)
<video
v-if="fileUrl"
ref="video"
class="file-preview video-preview"
controls
preload="meta"
muted="true"
playsinline="true"
webkit-playsinline="true"
x5-video-player-type="h5"
x5-playsinline="true"
controlsList="nodownload noremoteplayback noplaybackrate"
:disablePictureInPicture="true"
:src="fileUrl"
:poster="videoImgSrc"
>
<!--兼容ios -->
<source :src="fileUrl" type="video/mp4"/>
<source :src="fileUrl" type="video/ogg"/>
<source :src="fileUrl" type="video/webm"/>
</video>
// 图片预览通过v-model="fileList",fileList格式[{url: 'xxxx'}]
<van-uploader
class="upload-auth"
:max-count="1"
accept="image/*"
v-model="fileList"
:deletable="false"
:max-size="maxImgSize * 1024 * 1024"
:after-read="handleSuccess"
@oversize="onImgOversize"
>
</van-uploader>
安卓默认展示video视频播放控件, controlsList可以控制默认播放控件的显示隐藏, nodownload: 去除下载, noremoteplayback: 去除远程回放, noplaybackrate: 隐藏倍速, nofullscreen: 去除全屏, 设置属性:disablePictureInPicture="true"去除画中画,去除以上就不会显示右侧三个点。
或者通过css隐藏默认按钮展示:chrome 下,开发者工具 setting Preferences Elements 勾选 “Show user agent shadow DOM”可以查看video的内部构造, 参考: blog.csdn.net/weixin_4313…
// 隐藏video 当前时间按钮
video::-webkit-media-controls-current-time-display {
display: none;
}
// 隐藏video 音量按钮
video::-webkit-media-controls-mute-button {
display: none;
}
video::-webkit-media-controls-volume-control-container {
display: none;
}
video::-webkit-media-controls-toggle-closed-captions-button {
display: none;
}
//音量的控制条
video::-webkit-media-controls-volume-slider {
display: none;
}
// 隐藏video 总时间
video::-webkit-media-controls-time-remaining-display {
display: none;
}
// 隐藏video 全屏按钮
// video::-webkit-media-controls-fullscreen-button {
// display: none !important;
// }
video::--webkit-media-controls-play-button {
display: none !important;
-webkit-appearance: none !important;
}
ios默认全屏播放, 可以添加playsinline="true", webkit-playsinline="true"属性禁用默认全屏播放.
4.上传大文件没有file.content属性,会预览失败
拍摄视频/选择本地视频后, 小文件加载成功后会在file上添加content属性, 值是一个base64文件, 把这个base64文件赋值给video的src属性,即可实现预览. 但是大视频文件没有这个content属性, 会预览失败, 可以通过window.URL.createObjectURL(file.file)将视频文件转成Blob文件, 并赋值video的src属性来实现大文件预览.也可以直接用上传完oss后生成的url实现预览. 参考: blog.csdn.net/qq_42586895… 和 juejin.cn/post/684490…
this.fileUrl = window.URL.createObjectURL(file.file);
5.视频预览封面图获取
安卓可以通过监听video的loadeddata事件通过canvas获取视频指定帧数的封面图, 但是ios不会执行loadeddata事件, 也就获取不到封面图。我们知道视频播放后再暂停始终是有封面图的, 所以如果是微信环境的H5页面, 可以通过在window.WeixinJSBridge.invoke('getNetworkType', {}, function (e) {})回调里, 先触发播放视频, 再暂停视频播放来获取视频封面图。前提是要先设置视频静音即 muted="true" 参考: juejin.cn/post/684490…
// 视频截取封面图
watch: {
showUpload: {
handler(val) {
if(val) {
this.showProgress = false;
this.progress = 0;
this.$nextTick(() => {
let video = this.$refs.video;
video && this.setMedia();
})
}
}
}
},
setMedia(scale = 1) {
let video = this.$refs.video;
// 设置poster属性:(非本地视频资源会有跨域截图问题)
video.addEventListener('loadeddata', (e)=> {
setTimeout(()=>{
// 拿到图片
let canvas = this.$refs.canvas;
let ctx = canvas.getContext("2d");
// canvas.width = Math.ceil(video.offsetWidth /9) * 16 // 获取视频画面的真实宽度
canvas.width = video.offsetWidth * scale; // 获取视频画面的真实宽度
canvas.height = video.offsetHeight * scale;
video.currentTime = 1;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
let src = canvas.toDataURL('image/png');
// 设置属性
video.setAttribute('poster', src);
this.videoImgSrc = src;
},.5e3);
});
// (只在)微信环境自动播放
if (window.WeixinJSBridge) {
window.WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
try {
video.play();
// setTimeout(() => {
if (!video.paused) {
video.pause();
}
// })
} catch (error){
console.log(error)
}
}, false);
} else {
document.addEventListener("WeixinJSBridgeReady", function () {
window.WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
try {
video.play();
// setTimeout(() => {
if (!video.paused) {
video.pause();
}
// })
} catch (error){
console.log(error)
}
});
}, false);
}
},
6.van-progress上传视频上传进度值有变换, 但是进度条没有变化
除了设置:percentage="progress"外,还需要v-model="progress"绑定上传进度值,如果通过一个变量比如showProgress控制进度条显示隐藏, 不能使用v-show, 要用v-if ! 你说坑不坑。。。
<van-progress
v-if="showProgress"
class="progress-inner"
:show-pivot="false"
:percentage="progress"
v-model="progress"
/>
7.拍摄完视频先预览后点击按钮上传,安卓手机大文件会在拍摄完后会读取失败,导致页面卡顿或者闪退.
由于产品设计要求先预览视频,然后点击按钮才开始上传并查看上传进度,ios拍摄完会对大视频做压缩处理,所以能正常加载预览,但是安卓拍摄完成不会对视频压缩处理,文件过大就会加载失败,导致闪退。网上百度一些上传视频压缩的方法没有生效,暂时就直接这样处理了:大于500M视频拍摄完成后直接上传到oss,拿到url再预览,点击按钮直接上传到后台(如果有好的方式,麻烦大佬指点)。
// 上传拍摄视频
async beforeUpload(file) {
this.file = file;
let isLtM = file.size / 1024 / 1024 < this.maxSize;
if (!isLtM) {
this.$toast.fail(`请上传${this.maxSize / 1024}G以内的视频`);
return isLtM;
}
this.fileUrl = '';
this.showPicker = false;
this.loading = true;
// 安卓大文件上传,500M以上直接分片
if(file.size / 1024 / 1024 > 500) {
await this.multipartUpload(this.file);
}
return isLtM;
}
通过上面代码可以实现大视频成功上传预览,但是小文件上传会有下面报错:
排查发现把beforeUpload前面的async去掉就好了。。。
beforeReadVideo(file) {
this.file = file;
let isLtM = file.size / 1024 / 1024 < this.maxSize;
if (!isLtM) {
this.$toast.fail(`请上传${this.maxSize / 1024}G以内的视频`);
return isLtM;
}
this.fileUrl = '';
this.showPicker = false;
this.loading = true;
// 安卓大文件上传,500M以上直接分片
if(file.size / 1024 / 1024 > 500) {
this.multipartUpload(this.file).then((res) => {
return res;
}).catch(() => {
return false;
})
} else {
return isLtM;
}
},
// 分片上传
async multipartUpload(file) {
if (!this.ossClient) {
await this.initOSSClient();
}
console.log('分片上传:')
// const fileName = file.name;
return new Promise((resolve, reject) => {
let {tenantId} = JSON.parse(localStorage.getItem('leadsUserInfo'));
let {name} = file;
let lastName = name.replace(/[~!@#$%^+ ]/g,"");
let fileUuid = randomstring.generate(24);
let fileName = fileUuid + '-' + lastName;
try {
return this.ossClient.multipartUpload(`leads/${tenantId}/H5${fileName}`, file, {
parallel: this.parallel,
partSize: this.partSize,
progress: this.onMultipartUploadProgress
}).then(result => {
// 生成文件下载地址
// const url = `http://${this.bucket}.${this.region}.aliyuncs.com/leads/${tenantId}/H5/${fileName}`;
let { requestUrls = [] } = result.res || {}
let requestUrl = requestUrls.length ? requestUrls[0] : '';
let url = requestUrl.indexOf("?") > -1 ? requestUrl.substring(0, requestUrl.indexOf("?")) : requestUrl;
console.log(`分片上传 ${file.name} 成功, url === `, url, result);
this.successCallBack(url, resolve);
}).catch(err => {
console.log(`分片上传 ${file.name} 失败 === `, err);
this.errorCallBack(reject);
});
} catch (error) {
this.errorCallBack(reject);
}
});
},
// 上传成功
successCallBack(url = '', resolve) {
console.log('上传成功url:', url)
if(!url) {
return;
}
this.fileUrl = url;
this.loading = false;
this.fileList = [];
this.showUploadDialog = true;
resolve(true);
},
// 上传失败
errorCallBack(reject) {
this.$toast.fail("上传失败,请重新上传!");
this.closeLoadingDialog();
reject();
},