前端在上传视频时,需要截取视频中的 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, 0, 0, canvas.width, canvas.height)
// 转换为Base64格式的图像数据
const imageData = canvas.toDataURL('image/jpeg')
frames.value.push(imageData)
}
}
截了个黑图
一开始截取出一张黑色的图片,虽然也在 loadeddata 事件中加上了 refVideo.value.play(),貌似没有效果。

解决方案——在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 – 前端实现视频上传截取多帧画面、获取视频长度
暂无评论内容