一、 为什么要在前端做录制?
在传统的安防或直播业务中,视频录制通常由后端流媒体服务器完成。但在某些场景下(如用户想快速保存当前看到的画面、制作简短的证据片段),前端录制具有不可替代的优势:
- 即时性:所见即所得,无需等待服务器处理。
- 零服务器成本:利用客户端算力,不占用服务器磁盘和带宽。
- 灵活性:用户可以随时开始、随时停止。
二、 核心技术方案
在纯前端实现视频录制,最成熟且兼容性最好的方案是使用浏览器原生的 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>
五、 新手避坑指南
在实现过程中,有几个坑需要特别注意:
-
MIME Type 兼容性:
- 并不是所有浏览器都支持
video/webm;codecs=vp9。 - 解决方案:在代码中添加
MediaRecorder.isTypeSupported()检查,如果不支持高清格式,自动降级为普通video/webm。
- 并不是所有浏览器都支持
-
切换视频源:
- 当用户在录制过程中切换了摄像头,旧的流(Stream)会失效。
- 解决方案:在组件的
watch中监听视频源变化,如果正在录制,强制调用stopRecording()保存当前片段。
-
内存泄漏:
- 生成的
BlobURL (URL.createObjectURL) 会占用内存。 - 解决方案:下载触发后,务必调用
URL.revokeObjectURL(url)释放内存。
- 生成的