零基础通过 Vue 3 实现前端视频录制 —— 从原理到实战

一、 为什么要在前端做录制?

在传统的安防或直播业务中,视频录制通常由后端流媒体服务器完成。但在某些场景下(如用户想快速保存当前看到的画面、制作简短的证据片段),前端录制具有不可替代的优势:

  • 即时性:所见即所得,无需等待服务器处理。
  • 零服务器成本:利用客户端算力,不占用服务器磁盘和带宽。
  • 灵活性:用户可以随时开始、随时停止。

二、 核心技术方案

在纯前端实现视频录制,最成熟且兼容性最好的方案是使用浏览器原生的 MediaStream Recording API

1. 核心 API:MediaRecorder

你可以把它想象成浏览器内置的一个"录像机"。

  • 输入源 (Source):给它一个视频流(Stream),就像给录像机插上信号线。
  • 录制中 (Recording):它会将流数据不断地转换成二进制数据块(Chunks)。
  • 输出 (Output):当你喊"Cut"时,它将所有数据块拼接成一个完整的视频文件(Blob),供用户下载。

2. 数据源获取:captureStream

在我们的项目中,视频源来自于 <video> 标签播放的实时画面(包括 flv.js 解码后的画面)。我们使用 HTMLMediaElement.captureStream() 方法就能直接从 <video> 标签捕获当前播放的画面。

3. 文件格式

通常默认为 WebM 格式 (Chrome/Firefox),支持性最好。为了平衡画质和体积,我们优先尝试使用 video/webm;codecs=vp9 编码。

三、 业务逻辑设计

为了保证用户体验和程序的健壮性,在编码之前,我们需要设计好完整的业务逻辑:

1. 录制状态管理

  • 引入一个状态变量 isRecording (Boolean) 来标记当前是否正在录制。
  • UI 反馈:当处于录制状态时,按钮图标应变化(如变红或显示停止图标),文字变为"停止录制",给用户明确的反馈。

2. 交互流程

  • 点击录制按钮
    • 若未录制 :初始化 MediaRecorder,开始捕获流,置 isRecording = true
    • 若正在录制 :调用停止方法,导出文件,下载保存,置 isRecording = false

3. 异常与边界处理 (关键)

  • 切换视频源时 :如果用户在录制过程中切换了摄像头(即 <video>src 变了),必须自动停止当前录制并保存,否则流会中断或混合不同视频源的数据。
  • 页面销毁时 :Vue 组件销毁 (onUnmounted) 时需要检查是否在录制,如果是,则强制停止并保存,防止内存泄漏。
  • 无视频流时:如果当前没有播放视频,点击录制应提示"请先播放视频"。

四、 具体实现步骤

第一步:核心实现 useMediaRecorder.ts

它的职责单一且纯粹:只管录制,不管 UI

typescript 复制代码
// useMediaRecorder.ts
import { ref, onUnmounted, unref } from 'vue'
import type { Ref } from 'vue'

// 定义配置项接口
interface UseMediaRecorderOptions {
  mimeType?: string // 视频编码格式,如 'video/webm;codecs=vp9'
  filenamePrefix?: string // 下载文件的前缀
}

export function useMediaRecorder(
  // 接收一个响应式的 video 元素引用
  videoTarget: Ref<HTMLVideoElement | null> | HTMLVideoElement | null,
  options: UseMediaRecorderOptions = {}
) {
  const { mimeType = 'video/webm;codecs=vp9', filenamePrefix = 'record' } = options

  // 响应式状态:告诉外部当前是否正在录制
  const isRecording = ref(false)
  
  // 内部变量:录像机实例和数据仓库
  let mediaRecorder: MediaRecorder | null = null
  let recordedChunks: Blob[] = []

  // --- 核心动作:开始录制 ---
  const startRecording = () => {
    const videoEl = unref(videoTarget)
    if (!videoEl) return

    try {
      // 1. 获取"信号线":从 video 标签捕获流
      // 兼容性写法:不同浏览器 API 名称可能不同
      const stream = (videoEl as any).captureStream 
        ? (videoEl as any).captureStream() 
        : (videoEl as any).mozCaptureStream()

      if (!stream) throw new Error('无法获取视频流')

      // 2. 启动"录像机"
      // 这里可以做一些兼容性检查,如果不支持 VP9 就降级到普通 WebM
      mediaRecorder = new MediaRecorder(stream, { mimeType })

      // 3. 收集数据:每当有数据产生,就存入仓库
      mediaRecorder.ondataavailable = (event) => {
        if (event.data && event.data.size > 0) {
          recordedChunks.push(event.data)
        }
      }

      // 4. 停止时的处理:打包并下载
      mediaRecorder.onstop = () => {
        // 将所有碎片数据(Chunks)合并为一个大文件(Blob)
        const blob = new Blob(recordedChunks, { type: mimeType })
        // 创建下载链接
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = `${filenamePrefix}_${Date.now()}.webm`
        a.click() // 触发下载
        window.URL.revokeObjectURL(url) // 释放内存

        // 清空仓库,为下次录制做准备
        recordedChunks = []
        mediaRecorder = null
      }

      // 5. 正式开机
      mediaRecorder.start()
      isRecording.value = true
      console.log('开始录制视频')
    } catch (e) {
      console.error('录制启动失败:', e)
      console.error('录制失败,浏览器可能不支持')
    }
  }

  // --- 核心动作:停止录制 ---
  const stopRecording = () => {
    if (mediaRecorder && mediaRecorder.state !== 'inactive') {
      mediaRecorder.stop() // 这会触发上面的 onstop 事件
      isRecording.value = false
      console.log('录制已停止,正在下载...')
    }
  }

  // --- 自动护航:生命周期管理 ---
  // 如果组件被销毁了(用户切走了页面),录制会自动停止
  onUnmounted(() => {
    if (isRecording.value) {
      stopRecording()
    }
  })

  // 暴露出外部需要的方法和状态
  return {
    isRecording,
    startRecording,
    stopRecording
  }
}

第二步:在组件中使用

vue 复制代码
<!-- main.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useMediaRecorder } from '@renderer/composables/useMediaRecorder'

// 1. 获取 video 标签的引用
const videoPlayerRef = ref<HTMLVideoElement | null>(null)

// 2. 引入录制功能
const { 
  isRecording,       // 当前是不是在录制
  startRecording,    // 开始方法
  stopRecording      // 停止方法
} = useMediaRecorder(videoPlayerRef)

// 3. 按钮点击处理逻辑
const handleRecordClick = () => {
  if (isRecording.value) {
    stopRecording()
  } else {
    startRecording()
  }
}
</script>

<template>
  <!-- 绑定 ref -->
  <video ref="videoPlayerRef" ... ></video>

  <!-- 按钮样式随状态自动变化 -->
  <button 
    @click="handleRecordClick" 
    :class="{ 'red-btn': isRecording }"
  >
    {{ isRecording ? '停止录制' : '开始录制' }}
  </button>
</template>

五、 新手避坑指南

在实现过程中,有几个坑需要特别注意:

  1. MIME Type 兼容性

    • 并不是所有浏览器都支持 video/webm;codecs=vp9
    • 解决方案 :在代码中添加 MediaRecorder.isTypeSupported() 检查,如果不支持高清格式,自动降级为普通 video/webm
  2. 切换视频源

    • 当用户在录制过程中切换了摄像头,旧的流(Stream)会失效。
    • 解决方案 :在组件的 watch 中监听视频源变化,如果正在录制,强制调用 stopRecording() 保存当前片段。
  3. 内存泄漏

    • 生成的 Blob URL (URL.createObjectURL) 会占用内存。
    • 解决方案 :下载触发后,务必调用 URL.revokeObjectURL(url) 释放内存。
相关推荐
陈天伟教授5 分钟前
人工智能应用- 预测新冠病毒传染性:04. 中国:强力措施遏制疫情
前端·人工智能·安全·xss·csrf
zayzy25 分钟前
前端八股总结
开发语言·前端·javascript
今天减肥吗28 分钟前
前端面试题
开发语言·前端·javascript
Rabbit_QL40 分钟前
【前端UI行话】前端 UI 术语速查表
前端·ui·状态模式
小码哥_常1 小时前
一文带你吃透Android BLE蓝牙开发全流程
前端
小码哥_常1 小时前
从“新老交锋”看Retrofit与Ktor
前端
小J听不清1 小时前
CSS 外边距(margin)全解析:取值规则 + 实战用法
前端·javascript·css·html·css3
还是大剑师兰特2 小时前
Stats.js 插件详解及示例(完全攻略)
前端·大剑师·stats
前端小超超2 小时前
Vue计算属性computed:可写与只读的区别
前端·javascript·vue.js
IT_陈寒2 小时前
SpringBoot实战:3个隐藏技巧让你的应用性能飙升50%
前端·人工智能·后端