前置知识
利用renderjs在app端加载for web库 - 掘金 (juejin.cn)
需求背景
如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImage 和 uni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回内容中都没有提供视频封面图,于是只能使用一个固定的图片来充当视频封面,但是这样用户体验很不好
解决思路
在获取到视频链接后,如果我们可以让视频在后台自动播放,出现第一帧画面后再将它给停掉,在这个过程中利用 canvas
截取到视频播放的第一帧画面保存起来,那不就可以作为视频封面了吗?没那么容易,平时在 H5
环境中,到目前为止就行了,但问题是,现在我这里是 App
,然后 uni-app
自带的 video
组件没法截取画面,而 App
环境又没法用 H5
环境的 video
标签,它甚至没有 document
对象, 技术框架上不兼容, 那怎么办?
这时候就需要用到 renderjs 了,毕竟它的核心作用之一就是 "在视图层操作dom
,运行 for web
的 js
库"。
那思路就有了,在 renderjs
模块中监听原始模块中的文件列表,当更改时(新增、删除),在 renderjs
中动态创建 video
元素,让它自动静音播放,使用 canvas
截取第一帧画面后销毁 video
元素并将图片传递给原始模块,原始模块将其设置为对应视频的封面
代码逻辑
html
<template>
<view :prop="canvasList" :change:prop="canvas.getVideoCanvas">
<view v-for="(item,index) in fileList" :key="index">
<image v-if="item.type===0" :src="item.url" @click="previewImage(item.url)"></image>
<view v-else @click="previewVideoSrc = item.url">
<!-- 封面图 -->
<image mode="widthFix" :src="item.cover"></image>
<!-- 播放图标 -->
<u-icon class="play-icon" name="play-right-fill" size="30" color="#fff"></u-icon>
</view>
</view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close" @click="previewVideoSrc=''"> ×
</cover-view>
</video>
</view>
</view>
</template>
<script>
import { deepClone } from '@/utils'
// 原始模块
export default {
data() {
return {
previewVideoSrc: '', // 预览视频url
fileList: [
{ url: '', type: 0 },
{ url: '', type: 1 },
{ url: '', type: 1 },
] // 真正用来展示和传递的文件列表,type: 0代表图片,1代表视频
}
},
computed: {
// 用于 renderjs 模块监听,不用 fileList 是因为后续还有更改它(为其内部元素添加 cover )属性
// 监听 fileList 然后又更改它会导致循环递归,这里使用 deepClone 也是为了让 canvasList 不与
// fileList 产生关联
canvasList() {
return deepClone(this.fileList)
}
},
methods: {
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url]
});
},
// 生成视频封面
getVideoPoster({ index, cover }) {
this.$set(this.fileList[index], 'cover', cover)
},
}
}
</script>
<script module="canvas" lang="renderjs">
// renderjs 模块
export default {
methods: {
getVideoCanvas(nV, oV, ownerInstance) {
if(oV !== undefined && Array.isArray(nV) && nV.length > 0) {
nV.forEach((item, index) => {
// 如果是视频
if(item.type == 1) {
// 防止一次性执行过多逻辑导致卡顿
setTimeout(() => {
// 创建video标签
let video = document.createElement("video")
// 设置为自动播放和静音
video.setAttribute('autoplay', 'autoplay')
video.setAttribute('muted', 'muted')
// 设置播放源
video.innerHTML = '<source src=' + item.url + ' type="audio/mp4">'
// 创建 canvas 元素和 2d 画布
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// 监听 video 的 canplay 事件
video.addEventListener('canplay', function () {
// 设置宽高
let anw = document.createAttribute("width");
anw.nodeValue = 80;
let anh = document.createAttribute("height");
anh.nodeValue = 80;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
// 画布渲染
ctx.drawImage(video, 0, 0, 80, 80);
// 生成 base64 图片
let base64 = canvas.toDataURL('image/png')
// 暂停并销毁 video 元素
video.pause()
video.remove();
// 传递数据给逻辑层
ownerInstance.callMethod('getVideoPoster', {
index,
cover: base64
})
}, false)
}, index * 120)
}
})
}
}
}
}
</script>
成果展示
还有另一个地方,之前就是这样的,都是用的默认图片当作封面:
经过处理后就是这样啦: