uni-app利用renderjs实现截取视频第一帧画面作为封面图

前置知识

利用renderjs在app端加载for web库 - 掘金 (juejin.cn)

需求背景

如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImageuni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回内容中都没有提供视频封面图,于是只能使用一个固定的图片来充当视频封面,但是这样用户体验很不好

解决思路

在获取到视频链接后,如果我们可以让视频在后台自动播放,出现第一帧画面后再将它给停掉,在这个过程中利用 canvas 截取到视频播放的第一帧画面保存起来,那不就可以作为视频封面了吗?没那么容易,平时在 H5 环境中,到目前为止就行了,但问题是,现在我这里是 App,然后 uni-app 自带的 video 组件没法截取画面,而 App 环境又没法用 H5 环境的 video 标签,它甚至没有 document 对象, 技术框架上不兼容, 那怎么办?

这时候就需要用到 renderjs 了,毕竟它的核心作用之一就是 "在视图层操作dom,运行 for webjs库"。

那思路就有了,在 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>

成果展示

还有另一个地方,之前就是这样的,都是用的默认图片当作封面:

经过处理后就是这样啦:

相关推荐
CodeSheep3 分钟前
宇树科技,改名了!
前端·后端·程序员
Hilaku10 分钟前
为什么我们用了 Vite 还是构建慢?——真正的优化在这几步
前端·javascript·vite
XI锐真的烦11 分钟前
横向对比npm和yarn
前端·npm·node.js
国家不保护废物11 分钟前
🧩 React 组件化进阶:像乐高大师一样搭建你的应用世界!
前端·react.js·ai编程
TimelessHaze18 分钟前
从"切图崽"到前端工程师:React 到底是个啥?🚀
前端·react.js·ai编程
站在风口的猪110823 分钟前
《前端面试题:CSS的display属性》
前端·css·html·css3·html5
wandongle24 分钟前
HTML 面试题错题总结与解析
前端·面试·html
Code_Geo25 分钟前
前端打包工具简单介绍
前端·打包工具