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

22 阅读5分钟

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

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

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

二、 核心技术方案

在纯前端实现视频录制,最成熟且兼容性最好的方案是使用浏览器原生的 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

// 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
  }
}

第二步:在组件中使用

<!-- 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) 释放内存。