开箱即用!Vue3+TS 视频组件完整代码,自动提取视频第一帧做封面。妈妈再也不用担心我手动截封面了

最近继续开发 vx-sim 微信对话模拟器 gitee.com/maple2133/v... 这个项目,有兴趣的兄弟可以给帮忙点个Star⭐。

看了看微信,他的视频是显示的第一帧,然后加了一个播放图标。

简单实现了一下,后来想了想决定封装一下,免得大家有类似的需求还再鼓捣一遍。

用的是 Vue3+Ts 封装的自动提取视频第一帧作为封面的组件,纯前端实现、无第三方依赖,复制代码就能直接用。

核心原理:用"画布"给视频"拍照"

我们可以把视频想象成「一帧帧连续的图片合集」,而我们要做的,就是把这合集中的"第一张"(第一帧)单独抽出来,保存成图片。

这里用到了两个核心技术:HTML5的 video 标签和 canvas 标签。

实现步骤如下:

  1. 视频加载 :用户上传视频后,我们先通过 URL.createObjectURL() 生成一个临时的视频预览地址,让浏览器能加载并渲染这个视频。

  2. 定格第一帧 :视频加载完成后,我们把视频的播放进度调到0.1秒(注意这里必须是0.1秒),此时视频会定格在第一帧。

  3. 绘制封面 :用canvas画布,把视频当前定格的那一帧"画"下来,再通过 canvas.toDataURL() 把画布内容转成base64格式的图片。

  4. 交互优化:把封面图覆盖在视频上方,点击封面就播放视频,同时隐藏封面,还原自然的使用体验。

完整实现代码

html 复制代码
<template>
    <div class="video-upload-container">
        <!-- 隐藏原生文件上传框,用自定义样式触发 -->
        <input
            ref="fileInputRef"
            type="file"
            accept="video/*"
            class="file-input"
            @change="handleFileChange"
        />

        <!-- 视频预览区域:封面 + 视频播放器 -->
        <div v-if="videoInfo.url" class="video-preview">
            <!-- 视频播放器,原生控件,支持播放/暂停/进度条 -->
            <video
                ref="videoRef"
                class="video-player"
                :src="videoInfo.url"
                controls
                preload="auto"
                @loadeddata="handleVideoLoaded"
            >
                您的浏览器不支持视频播放,请升级浏览器
            </video>

            <!-- 封面图:默认覆盖在视频上,点击播放后隐藏 -->
            <img
                v-if="videoInfo.cover"
                :src="videoInfo.cover"
                class="video-cover"
                @click="handlePlayVideo"
                alt="视频封面"
            />
        </div>

        <!-- 无视频时的占位提示,点击触发上传 -->
        <div v-else class="upload-placeholder" @click="triggerFileInput">
            <span>点击上传视频</span>
        </div>
    </div>
</template>

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

interface VideoInfo {
    url: string // 视频本地预览地址(临时URL)
    cover: string // 封面图base64地址
}

const videoRef = ref<HTMLVideoElement | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)

// 响应式存储视频和封面信息
const videoInfo = reactive<VideoInfo>({
    url: '',
    cover: ''
})

/**
 * 触发文件选择框(自定义占位区点击时调用)
 */
const triggerFileInput = () => {
    // 模拟点击原生文件输入框
    fileInputRef.value?.click()
}

/**
 * 处理文件选择变化(用户上传视频后触发)
 */
const handleFileChange = (e: Event) => {
    const target = e.target as HTMLInputElement
    const file = target.files?.[0]
    if (!file) return // 没有选择文件则退出

    // 生成视频本地预览地址:浏览器临时生成的URL,仅当前页面有效
    videoInfo.url = URL.createObjectURL(file)
}

/**
 * 视频加载完成后,提取第一帧作为封面
 * 监听video的loadeddata事件(视频元数据加载完成,可开始渲染)
 */
const handleVideoLoaded = () => {
    const video = videoRef.value
    if (!video) return

    // 解决跨域截图失败问题:允许跨域访问视频资源
    video.crossOrigin = 'anonymous'

    // 关键操作:将视频进度设为0.1秒(避免0秒时黑屏,后面讲坑点)
    video.currentTime = 0.1

    // 监听视频的canplay事件(视频可正常播放,此时帧已渲染完成)
    video.oncanplay = () => {
        // 调用截图方法,生成封面
        captureVideoCover(video)
    }
}

/**
 * 核心方法:截取视频当前帧,生成封面图
 * @param video 视频DOM元素
 */
const captureVideoCover = (video: HTMLVideoElement) => {
    // 创建canvas画布(相当于"拍照的底片")
    const canvas = document.createElement('canvas')
    // 获取画布的2D绘图上下文(相当于"画笔")
    const ctx = canvas.getContext('2d')
    if (!ctx) return // 浏览器不支持canvas则退出

    // 关键:设置画布尺寸与视频实际尺寸一致,避免封面拉伸变形
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight

    // 把视频当前帧画到画布上(x,y坐标从0开始,宽高与画布一致)
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height)

    // 将画布内容转成base64格式的图片(jpeg格式,画质0.8,可调整)
    videoInfo.cover = canvas.toDataURL('image/jpeg', 0.8)

    // 优化体验:截图完成后,暂停视频并回到初始位置
    video.pause()
    video.currentTime = 0
}

/**
 * 点击封面图,播放视频并隐藏封面
 */
const handlePlayVideo = () => {
    const video = videoRef.value
    if (!video) return

    video.play() // 播放视频
    videoInfo.cover = '' // 隐藏封面
}
</script>

<style scoped>
.video-upload-container {
    width: 100%;
    max-width: 600px;
    margin: 0 auto;
}

.file-input {
    display: none;
}

.upload-placeholder {
    width: 100%;
    height: 320px;
    border: 2px dashed #ccc;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: #666;
    font-size: 16px;
    transition: border-color 0.3s;
}

.upload-placeholder:hover {
    border-color: #42b983;
}

.video-preview {
    position: relative;
    width: 100%;
    height: 320px;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.video-player {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.video-cover {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    cursor: pointer;
    transition: opacity 0.3s;
}

.video-cover:hover {
    opacity: 0.9;
}
</style>

避坑!避坑!避坑!

一定要把currentTime设为0.1秒

如果把video.currentTime设为0秒,很多时候会出现"封面黑屏"的情况。

原因是:视频的0秒的帧可能还没加载完成,或者部分视频的0秒帧是黑屏(比如视频开头的过渡帧)。

把时间设为0.1秒,相当于"等视频加载完第一帧后再截图",既能保证帧已渲染完成,又能避免黑屏问题。

视频跨域

如果你的视频是从服务器加载的(不是本地上传),可能会出现"canvas截图失败"的情况,控制台会报跨域错误。

这是因为浏览器的同源策略限制:canvas不能绘制跨域的资源。

加上 video.crossOrigin = "anonymous";,相当于告诉浏览器"允许跨域访问这个视频资源"。

另外配合服务器设置CORS(跨域资源共享),就能解决跨域截图的问题。

如果是本地上传的视频,这行代码也可以加上,提前规避问题。

封面拉伸变形

canvas的宽高必须设置为 video.videoWidthvideo.videoHeight(视频实际宽高),而不是固定值,防止出现拉伸变形。

总结

其实这个视频组件的封装,核心就是"利用HTML5原生API,把复杂的操作拆分成简单的步骤":

上传视频 → 加载视频 → 截取帧 → 生成封面 → 优化交互。

没有复杂的第三方依赖,全靠原生API和Vue3的组合式API就能实现。

相关推荐
盐多碧咸。。1 小时前
echarts折线图矩形选择 框选图表
前端·javascript·echarts
羽沢311 小时前
Canvas学习一
前端·css·学习·canvas
KaMeidebaby2 小时前
卡梅德生物技术快报|锦葵科植物遗传转化工程化优化:棉花胚尖农杆菌转化体系参数固化与效率提升
前端
invicinble2 小时前
前端框架使用vue-cli( 第二层:工程配置层--4.axios需要做的基础配置)
前端·vue.js·前端框架
用户070455741292 小时前
第一次前后端联调后,我终于理解了什么是工程化
前端
亲亲小宝宝鸭2 小时前
Vue3中那些冷门但实用的方法
前端·vue.js
qq_349523262 小时前
分析原型到表的过程
前端
2 小时前
Pinia 全局状态管理
前端
M ? A2 小时前
Vue 转 React | VuReact 实时监听开发指南
前端·vue.js·后端·react.js·面试·开源·vureact