30 – 前端实现视频上传截取多帧画面、获取视频长度

30 - 前端实现视频上传截取多帧画面、获取视频长度

前端在上传视频时,需要截取视频中的 N 帧画面,选择一张做封面。

通过监听 video,我们可以使用 canvas 绘制当前视频位置的画面,并输出 base64 图片数据。虽然实现起来不难,我还是走了一些弯路。

上传后截取视频

最开始的做法是在视频上传后截屏。根据后端返回的视频地址,初始化 video,并监听这个 video 的 loadeddata 事件,可以截取视频加载完成后的画面。

  // 上传成功后,截图视频画面
  const onSuccess()=>{
      // ...
      // 设置视频源地址
      src.value = videoSrc.value

      // 加载成功后截取
      refVideo.value.onloadeddata = function ({
         refVideo.value.play()
         captureFrame()   
      }
      // ...
  }
  
  // 截取视频帧
  const captureFrame = () => {
      // 创建Canvas元素
      const canvas = document.createElement('canvas')
      // 设置Canvas大小与视频相同
      canvas.width = refVideo.value.clientWidth
      canvas.height = refVideo.value.clientHeight
      // 获取Canvas上下文
      const context = canvas.getContext('2d'as CanvasRenderingContext2D
      // 将视频内容绘制到Canvas上
      context.drawImage(refVideo.value, 00, canvas.width, canvas.height)
      // 转换为Base64格式的图像数据
      const imageData = canvas.toDataURL('image/jpeg')
      frames.value.push(imageData)
   }
}

截了个黑图

一开始截取出一张黑色的图片,虽然也在 loadeddata 事件中加上了 refVideo.value.play(),貌似没有效果。

30 - 前端实现视频上传截取多帧画面、获取视频长度
截了一个黑图

解决方案——在video添加属性 autoplay 和 muted,即加载后立即静音播放,可以成功截取。

 <video :src="src" controls ref="refVideo" class="w-80 h-45" autoplay muted></video>

截取多张图片

实际场景需要截取多张图片以供用户挑选, 比如需要在视频中截取 4 张图片,应该如何实现呢?

通过修改 video 的 currentTime 可以让视频跳转到对应播放位置,然后截取对应的画面。

currentTime 属性会以秒为单位返回当前媒体元素的播放时间。设置这个属性会改变媒体元素当前播放位置。

跳转到对应位置后,执行 seeked 事件

seeked 拖动进度(seek)操作完成。

一开始我使用 setTimeout 每隔200ms,修改 currentTime,然后监听 seek事件 在视频文件不大,网速好的情况下,可以获取到想要的 4 个帧。

但是当拿出2G的视频测试时,发现并不能成功获取 4 个,有时是2个,有时是3个,为什么呢?

因为seek 事件需要在拖动进度完成时,虽然设置了200ms的时间间隔,但是可能视频进度拖动还没完成,这时还没有成功截取视频帧,就开始跳下一个位置。

也就是说,需要在跳转到对应时间点后,并加载成功开始播放,才能成功截取视频。4 个截取位置,需要前一个截取成功后,再执行下一个。

video 提供了另一个事件 playing,试试监听它

playing 事件在播放准备开始时(之前被暂停或者由于数据缺乏被暂缓)被触发。

const FRAME_COUNT = 4
let index = 1
refVideo.value?.addEventListener('playing', handlePlaying)

const handlePlaying = () => {
  const currentTime = (duration.value / (FRAME_COUNT + 1)) * index
  refVideo.value.currentTime = currentTime
  nextTick(() => {
    captureFrame()
    index++

    // 截取完成,移除事件监听
    if (index > FRAME_COUNT) {
      refVideo.value?.removeEventListener('playing', handlePlaying)
    }
  })
}

虽然可以成功截取,但是当视频文件较大,网速不好时,截屏所需时间也比较久,只能在视频不大时凑合使用,现在视频文件动辄 几百兆、几 G ,这样的代码实现肯定不过关,无法交付生产。

优化版,体验大幅提升

在视频上传完成后,获取视频URL来实现截屏,受制于网速、视频大小,截图的速度非常不稳定,甚至让人难以忍受,必须优化!

选取视频后就可以截屏

选取视频后,上传到服务器之前,我们可以获取选中视频的本地blob数据,从而使用video进行播放并截屏。使用本地视频截图,不受网速和视频大小影响,速度提升不是一点点。

接下来需要做的就是在上传前(使用 el-upload 组件的 beforeUpload ) 获取本地视频长度、完成截屏操作。

// 在el-upload 的 beforeUpload之前获取视频长度、截屏
const beforeUpload = (file: Blob) => {
  index = 1
  src.value = ''
  frames.value.length = 0

  // 设置视频源
  src.value = window.URL.createObjectURL(file)

  refVideo.value.onloadeddata = function ({
    // 获取视频的长度(s)
    duration.value = refVideo.value.duration

    nextTick(() => {
      // 监听seeked和playing都可以,但是seeked需要手动修改一次currentTime,才能监听到seeked事件,还是playing更省事
      // refVideo.value.currentTime = 1
      refVideo.value?.addEventListener('playing', handlePlaying)
    })
  }
}

playing 浏览器兼容性问题

在选择视频后立即执行截屏,几乎是秒速实现,但是进行浏览器兼容性测试时,chrome 和 edge 通过,而 safari,360极速浏览器无法执行,why?

之前说到监听 seeked 事件也可以,那就把 playing 换成 seeked 试试,结果真行了。

const beforeUpload = (file: Blob) => {
    // ...
    nextTick(() => {     
      // seeked需要手动修改一次currentTime,才能监听到seeked事件
      // refVideo.value.currentTime = 1
      refVideo.value?.addEventListener('seeked', handlePlaying)
    })
}
    
const handlePlaying = () => {
    // ...
    // 截取完成,移除事件监听
    if (index > FRAME_COUNT) {
        refVideo.value?.removeEventListener('seeked', handlePlaying)
    } 
}

完整实现代码

OK,到这一步,已经基本完成需求,以下是实现代码:

<template>
  <div class="container">
    <el-card>
      <template #header> 视频文件上传 </template>
      <el-upload
        id="upload"
        ref="refUpload"
        action="http://jsonplaceholder.typicode.com/api/posts/"
        :before-upload="beforeUpload"
      >

        <el-button>上传视频</el-button>
      </el-upload>
      <div v-show="src">
        <video :src="src" controls ref="refVideo" class="w-80 h-45" autoplay muted></video>

        <div class="my-4">视频长度:{{ duration }} 秒</div>
        <ul class="flex flex-wrap">
          <li v-for="frame in frames" class="w-40 p-1 h-23"><el-image :src="frame" /></li>
        </ul>
      </div>
    </el-card>
  </div>

</template>

<script setup lang="ts">
import { ref, nextTick } from 'vue'

/
/ 需要截取的帧数
const FRAME_COUNT = 8

const refUpload = ref()
const refVideo = ref()
const src = ref('')
const duration = ref(0)
const frames = ref<string[]>([])

/
/ 截取视频帧
const captureFrame = () => {
  /
/ 创建Canvas元素
  const canvas = document.createElement('canvas')
  /
/ 设置Canvas大小与视频相同
  canvas.width = refVideo.value.clientWidth
  canvas.height = refVideo.value.clientHeight
  /
/ 获取Canvas上下文
  const context = canvas.getContext('2d') as CanvasRenderingContext2D
  /
/ 将视频内容绘制到Canvas上
  context.drawImage(refVideo.value, 0, 0, canvas.width, canvas.height)
  /
/ 转换为Base64格式的图像数据
  const imageData = canvas.toDataURL('image/
jpeg')
  frames.value.push(imageData)
}

let index = 1

const handlePlaying = () => {
  const currentTime = (duration.value / (FRAME_COUNT + 1)) * index
  refVideo.value.currentTime = currentTime
  nextTick(() => {
    captureFrame()
    index++

    // 截取完成,移除事件监听
    if (index > FRAME_COUNT) {
      refVideo.value?.removeEventListener('
seeked', handlePlaying)
    }
  })
}

const beforeUpload = (file: Blob) => {
  index = 1
  src.value = '
'
  frames.value.length = 0

  // 设置视频源
  src.value = window.URL.createObjectURL(file)

  refVideo.value.onloadeddata = function () {
    // 获取视频的长度(s)
    duration.value = refVideo.value.duration

    nextTick(() => {
      refVideo.value.currentTime = 1
      // seeked需要手动修改一次currentTime,才能监听到seeked事件,
      refVideo.value?.addEventListener('
seeked', handlePlaying)
    })
  }
}
</script>


结合实际情况,可以继续优化改进。欢迎交流。

项目地址

本项目GIT地址:https://github.com/lucidity99/mocha-vue3-system

原文始发于微信公众号(自由前端之路):30 – 前端实现视频上传截取多帧画面、获取视频长度

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容