一、 为什么要在前端做录制?
在传统的安防或直播业务中,视频录制通常由后端流媒体服务器完成。但在某些场景下(如用户想快速保存当前看到的画面、制作简短的证据片段),前端录制具有不可替代的优势:
- 即时性:所见即所得,无需等待服务器处理。
- 零服务器成本:利用客户端算力,不占用服务器磁盘和带宽。
- 灵活性:用户可以随时开始、随时停止。
二、 核心技术方案
在纯前端实现视频录制,最成熟且兼容性最好的方案是使用浏览器原生的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.tsimport{ref,onUnmounted,unref}from'vue'importtype{Ref}from'vue'// 定义配置项接口interfaceUseMediaRecorderOptions{mimeType?:string// 视频编码格式,如 'video/webm;codecs=vp9'filenamePrefix?:string// 下载文件的前缀}exportfunctionuseMediaRecorder(// 接收一个响应式的 video 元素引用videoTarget:Ref<HTMLVideoElement|null>|HTMLVideoElement|null,options:UseMediaRecorderOptions={}){const{mimeType='video/webm;codecs=vp9',filenamePrefix='record'}=options// 响应式状态:告诉外部当前是否正在录制constisRecording=ref(false)// 内部变量:录像机实例和数据仓库letmediaRecorder:MediaRecorder|null=nullletrecordedChunks:Blob[]=[]// --- 核心动作:开始录制 ---conststartRecording=()=>{constvideoEl=unref(videoTarget)if(!videoEl)returntry{// 1. 获取“信号线”:从 video 标签捕获流// 兼容性写法:不同浏览器 API 名称可能不同conststream=(videoElasany).captureStream?(videoElasany).captureStream():(videoElasany).mozCaptureStream()if(!stream)thrownewError('无法获取视频流')// 2. 启动“录像机”// 这里可以做一些兼容性检查,如果不支持 VP9 就降级到普通 WebMmediaRecorder=newMediaRecorder(stream,{mimeType})// 3. 收集数据:每当有数据产生,就存入仓库mediaRecorder.ondataavailable=(event)=>{if(event.data&&event.data.size>0){recordedChunks.push(event.data)}}// 4. 停止时的处理:打包并下载mediaRecorder.onstop=()=>{// 将所有碎片数据(Chunks)合并为一个大文件(Blob)constblob=newBlob(recordedChunks,{type:mimeType})// 创建下载链接consturl=URL.createObjectURL(blob)consta=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=trueconsole.log('开始录制视频')}catch(e){console.error('录制启动失败:',e)console.error('录制失败,浏览器可能不支持')}}// --- 核心动作:停止录制 ---conststopRecording=()=>{if(mediaRecorder&&mediaRecorder.state!=='inactive'){mediaRecorder.stop()// 这会触发上面的 onstop 事件isRecording.value=falseconsole.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)释放内存。
- 生成的