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>

成果展示

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

经过处理后就是这样啦:

相关推荐
qq_5896660532 分钟前
TypeScript 完整入门教程
前端·javascript·typescript
2501_9159184136 分钟前
深入对比iOS开发中常用性能监控工具的底层原理与优缺点分析
android·ios·小程序·https·uni-app·iphone·webview
星栈独行2 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
开发语言·javascript·人工智能·程序人生
落落Plus3 小时前
浏览器缓存机制详解
javascript·缓存·浏览器缓存
用户059540174464 小时前
LLM对话记忆测试踩坑实录:手工回归30分钟,自动化后2分钟发现3个隐藏Bug
前端·css
Ashley的成长之路5 小时前
前端性能优化实战手册·第2篇:资源加载策略全解
前端·性能优化·资源加载·http/3·性能优化实战·资源加载优化
浅水壁虎5 小时前
vue基础(第二章 )
前端·javascript·vue.js
界面开发小八哥5 小时前
界面控件DevExtreme v26.1新版亮点——支持Angular 22
前端·javascript·angular.js·devexpress·ui开发·devextreme
Getflare5 小时前
前端 + UI 设计 + AI:这不是三个工种,是一个新三角能力模型(附自检清单)
前端·人工智能·ui