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>

成果展示

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

经过处理后就是这样啦:

相关推荐
梦曦i3 小时前
@meng-xi/create-uni-app vs unibest:轻量脚手架与重型框架的定位之争
前端·uni-app
平头哥技术团队3 小时前
Day 10 | 工欲善其事:VS Code 配置与项目归档
android·开发语言·前端·javascript·html·交互
乌恩大侠4 小时前
GH200 新增 NVMe SSD 分区、格式化与挂载完整流程
运维·服务器·前端
yume_sibai4 小时前
Element Plus 数据展示组件完全指南(15个核心组件)
前端·javascript·vue.js
qiuhaipeng14 小时前
Claude code 升级后上下文长度变短问题
java·前端·数据库
zzz_23684 小时前
个人 AI 记忆如何跨工具复用:用 Markdown、索引和 Skill 搭一个可治理的记忆库
前端·人工智能·react.js·前端框架·agent·agent测评
剑之所向6 小时前
.NET 官方`System.Threading.Channels`
前端·javascript·数据库
计算机魔术师7 小时前
GPT-6 Astra幻觉砍到2%,却被一种老招数轻松绕过
前端
cjy0001117 小时前
2026AI 产品如何从一次性 Demo 变成可重复使用的业务工具?
前端·人工智能·fde
IT_陈寒7 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端