van-uploader+oss分片上传/拍摄视频+预览

1,543 阅读2分钟

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;
}

通过上面代码可以实现大视频成功上传预览,但是小文件上传会有下面报错:

image.png

src=http___www.54u.net_wp-content_uploads_2019_07_RV4GtzbCVk6S1s.jpg&refer=http___www.54u.webp

排查发现把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();
},