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>

成果展示

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

经过处理后就是这样啦:

相关推荐
玫城27 分钟前
[ VUE ] 封装通用数组校验组件,el-input内使用
前端·javascript·vue.js
弓.长.5 小时前
React Native 鸿蒙跨平台开发:实现一个多功能单位转换器
javascript·react native·react.js
南半球与北海道#5 小时前
前端打印(三联纸票据打印)
前端·vue.js·打印
摘星编程5 小时前
React Native for OpenHarmony 实战:ToggleSwitch 切换开关详解
javascript·react native·react.js
董世昌415 小时前
深入浅出 JavaScript 常用事件:从原理到实战的全维度解析
前端
满栀5855 小时前
分页插件制作
开发语言·前端·javascript·jquery
qq_406176146 小时前
深入剖析JavaScript原型与原型链:从底层机制到实战应用
开发语言·前端·javascript·原型模式
弓.长.6 小时前
React Native 鸿蒙跨平台开发:BottomSheet 底部面板详解
javascript·react native·react.js
开开心心_Every6 小时前
免费窗口置顶小工具:支持多窗口置顶操作
服务器·前端·学习·macos·edge·powerpoint·phpstorm
摘星编程6 小时前
React Native for OpenHarmony 实战:Permissions 权限管理详解
javascript·react native·react.js