基于RecordRTC实现WebRTC媒体流的实时录制与回放
1. 从零开始:为什么你需要RecordRTC?
如果你正在开发一个在线课堂、视频会议或者直播连麦的应用,你可能会遇到一个非常实际的需求:怎么把用户正在进行的音视频通话给录下来? 用户可能想回看错过的精彩讨论,或者讲师需要把课程内容存档。这时候,你可能会想到服务器录制,但那需要复杂的后端架构和大量的带宽成本。有没有一种更轻量、更直接的办法呢?
当然有,答案就是 WebRTC 配合 RecordRTC。简单来说,WebRTC 让浏览器能直接进行点对点的音视频通信,而 RecordRTC 则是一个运行在浏览器里的“录音录像机”,它能直接捕获这些通信流,并在用户本地生成视频文件。整个过程完全在客户端完成,不经过你的服务器,既减轻了你的服务器压力,也保护了用户的隐私。
我刚开始接触这个需求时,也尝试过其他方案,比如用 MediaRecorder API。但实测下来,MediaRecorder 的浏览器兼容性是个大坑,尤其是在处理复杂的、包含多个轨道的 MediaStream 时,很容易出问题。而 RecordRTC 这个库,可以说是专门为 WebRTC 场景而生,它底层做了大量的兼容性处理和优化,把那些繁琐的细节都封装好了,让你用几行代码就能实现稳定录制。踩过坑之后,我果断选择了它。
所以,无论你是想给视频会议加个“录制会议”按钮,还是为在线教育平台增加课程回放功能,基于 RecordRTC 的客户端录制方案都是一个非常值得考虑的起点。它上手快,对后端依赖小,特别适合作为功能的快速原型或者对实时性要求高的场景。
2. 环境准备与第一个录制Demo
理论说再多,不如动手跑一遍。我们先来把环境搭起来,实现一个最基础的“录制摄像头并下载”的功能。这个过程你会清晰地看到 RecordRTC 的工作流程。
2.1 安装与引入RecordRTC
首先,我们需要把 RecordRTC 库拿到我们的项目里。最推荐的方式是通过 npm 安装,这样便于版本管理。
在你的项目根目录下打开终端(命令行),执行:
npm install recordrtc
安装完成后,你会在 node_modules 文件夹里找到 RecordRTC 的核心文件。接下来,创建一个简单的 HTML 文件,比如叫 index.html。在文件中,你需要引入这个库。注意,RecordRTC 本身并不依赖其他大型框架,直接引入即可。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>RecordRTC 基础录制Demo</title>
</head>
<body>
<h2>我的第一个录制应用</h2>
<button id="btn-start">开始录制</button>
<button id="btn-stop" disabled>停止录制</button>
<a id="link-download" style="display: none;">下载视频</a>
<hr>
<!-- 这个video用于实时预览摄像头 -->
<video id="liveVideo" autoplay playsinline muted width="640"></video>
<hr>
<!-- 这个video用于回放刚刚录制的视频 -->
<video id="playbackVideo" controls width="640"></video>
<!-- 引入RecordRTC库 -->
<script src="./node_modules/recordrtc/RecordRTC.js"></script>
<script src="./js/app.js"></script> <!-- 我们自己的逻辑写在这里 -->
</body>
</html>
注意:
playsinline属性在移动端浏览器上很重要,它允许视频在内联框架中播放,而不是自动全屏。muted属性是为了避免 Chrome 等浏览器的自动播放策略,因为播放带声音的媒体需要用户先交互。
2.2 核心录制逻辑剖析
现在,我们来创建 js/app.js 文件,编写真正的录制逻辑。我会把代码拆解开,一步步解释。
第一步:获取用户的媒体流(摄像头和麦克风)。
这是 WebRTC 的标准 API,getUserMedia 会向用户请求权限。
// 获取DOM元素
const liveVideo = document.getElementById('liveVideo');
const playbackVideo = document.getElementById('playbackVideo');
const startBtn = document.getElementById('btn-start');
const stopBtn = document.getElementById('btn-stop');
const downloadLink = document.getElementById('link-download');
let mediaStream = null; // 保存获取到的音视频流
let recorder = null; // 保存RecordRTC的录制器实例
// 定义我们想要获取的媒体类型,这里同时要视频和音频
const constraints = {
video: true,
audio: true
};
// 开始录制按钮的点击事件
startBtn.onclick = async function() {
try {
// 1. 请求摄像头和麦克风权限,并获得媒体流
mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
// 2. 将媒体流实时显示在“直播”video标签上
liveVideo.srcObject = mediaStream;
// 3. 创建RecordRTC录制器实例
// 关键配置:type: 'video' 表示录制音视频。也可以是 'audio' 只录音。
recorder = RecordRTC(mediaStream, {
type: 'video',
mimeType: 'video/webm;codecs=vp9', // 指定编码格式,可选
recorderType: MediaStreamRecorder, // 指定录制器类型,可选
timeSlice: 1000, // 可选:每隔1秒生成一个Blob片段,用于实时上传
ondataavailable: function(blob) {
// 如果设置了timeSlice,这个回调会定期触发
console.log('收到一段数据Blob:', blob);
}
});
// 4. 开始录制
recorder.startRecording();
// 5. 更新按钮状态
startBtn.disabled = true;
stopBtn.disabled = false;
console.log('录制已开始!');
} catch (error) {
console.error('无法获取媒体设备:', error);
alert('无法访问摄像头或麦克风,请检查权限和设备。');
}
};
第二步:停止录制并处理结果。 当用户点击停止时,我们需要结束录制过程,并拿到最终的视频文件。
// 停止录制按钮的点击事件
stopBtn.onclick = function() {
if (!recorder) {
return;
}
this.disabled = true;
console.log('正在停止录制并处理数据...');
// 停止录制。停止后,会触发回调函数。
recorder.stopRecording(function() {
// 这个回调函数在录制完全停止、数据就绪后执行
// 1. 从录制器中获取最终的视频Blob对象
const videoBlob = recorder.getBlob();
console.log('录制完成,Blob类型:', videoBlob.type, '大小:', videoBlob.size);
// 2. 为这个Blob创建一个本地URL,用于视频回放和下载
const videoURL = URL.createObjectURL(videoBlob);
// 3. 将回放video标签的源设置为这个URL
playbackVideo.src = videoURL;
playbackVideo.load(); // 加载视频
// 4. 设置下载链接
downloadLink.href = videoURL;
downloadLink.download = `录制_${new Date().getTime()}.webm`; // 动态生成文件名
downloadLink.style.display = 'inline-block'; // 显示下载链接
downloadLink.textContent = '点击下载视频文件 (.webm)';
// 5. 清理工作:停止媒体流的所有轨道
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
liveVideo.srcObject = null; // 清空直播预览
}
// 6. 销毁录制器实例,释放内存
recorder.destroy();
recorder = null;
mediaStream = null;
startBtn.disabled = false;
console.log('录制流程结束。');
});
};
到这里,一个最基础但完全可用的录制应用就完成了。打开你的 index.html 文件,点击“开始录制”,允许浏览器使用摄像头和麦克风,然后你会在上方看到实时画面。点击“停止录制”后,下方的播放器就会自动加载并播放你刚刚录制的视频,同时会出现一个下载链接。
提示:默认生成的是
.webm格式文件,这是一种开放的、浏览器原生支持良好的容器格式。如果你需要mp4格式,需要在服务器端或使用 WebAssembly 进行转码,这是另一个话题了。
3. 进阶实战:录制与回放WebRTC通话流
上面的例子录制的是本地摄像头流。但在真实场景中,比如视频会议,我们需要录制的是 WebRTC 对等连接(PeerConnection) 传输的远程媒体流。别担心,原理是相通的,只是数据源变了。
3.1 理解流(Stream)与轨道(Track)
在 WebRTC 中,一个 MediaStream 可以包含多个轨道(MediaStreamTrack),比如一个音频轨道和一个视频轨道。当我们建立 RTCPeerConnection 后,通过 ontrack 事件或者 pc.getReceivers() 可以获取到远程传来的流。
假设我们已经建立了一个简单的点对点连接,并收到了远程流:
// 假设 peerConnection 是已经建立好的 RTCPeerConnection 实例
let remoteStream = null;
peerConnection.ontrack = function(event) {
if (event.streams && event.streams[0]) {
// 通常,远程流会通过 event.streams 传递过来
if (!remoteStream) {
remoteStream = event.streams[0];
console.log('收到远程流,轨道:', remoteStream.getTracks());
// 可以将其显示在一个video标签上
document.getElementById('remoteVideo').srcObject = remoteStream;
// !!!关键步骤:开始录制这个远程流
startRecordingRemoteStream(remoteStream);
}
}
};
3.2 封装录制远程流的函数
录制远程流和录制本地流在代码上几乎一模一样,唯一的区别是流的来源。
let remoteRecorder = null;
function startRecordingRemoteStream(stream) {
// 确保流有效且有轨道
if (!stream || stream.getTracks().length === 0) {
console.warn('无效的媒体流,无法开始录制。');
return;
}
// 创建录制器。可以配置更多参数以适应通话场景。
remoteRecorder = RecordRTC(stream, {
type: 'video',
mimeType: 'video/webm;codecs=vp8', // VP8编码兼容性更好
bitsPerSecond: 128000, // 设置比特率,控制视频质量与大小
// 通话录制通常需要分片上传,timeSlice 非常有用
timeSlice: 5000, // 每5秒生成一个数据片段
ondataavailable: function(blob) {
// 在这里,你可以将 blob 实时上传到服务器
// 例如:uploadChunk(blob);
console.log(`收到分片数据,序号: ${Date.now()},大小: ${blob.size} bytes`);
}
});
remoteRecorder.startRecording();
console.log('远程流录制已开始。');
}
// 停止录制远程流的函数
function stopRecordingRemoteStream() {
if (remoteRecorder) {
remoteRecorder.stopRecording(function() {
const finalBlob = remoteRecorder.getBlob();
// 处理最终合并的完整视频文件
saveOrUploadFinalRecording(finalBlob);
remoteRecorder.destroy();
remoteRecorder = null;
});
}
}
这里有一个非常重要的点: 在视频会议中,你可能想同时录制本地和远程的画面,甚至把它们合成一个画中画。这就需要你分别获取本地流和远程流,然后使用 Canvas 或者更高级的 API(如 MediaStream 的 addTrack/removeTrack)来合成一个新的流,再用 RecordRTC 去录制这个合成流。这涉及到更复杂的媒体处理,但 RecordRTC 作为录制工具,始终是最后、最可靠的一环。
3.3 实现即时回放功能
“回放”不一定非要等整个录制结束。在在线教育场景,老师可能想立刻回顾刚才自己讲的一段话。我们可以实现一个“缓冲回放”功能,比如回放过去10秒的内容。
这个功能需要利用 timeSlice 和 ondataavailable。思路是维护一个固定长度的数据块队列。
class ReplayBuffer {
constructor(bufferDuration = 10000) { // 默认缓冲10秒
this.chunks = []; // 存储数据块的数组
this.bufferDuration = bufferDuration;
this.recorder = null;
}
start(stream) {
this.recorder = RecordRTC(stream, {
type: 'video',
mimeType: 'video/webm',
timeSlice: 1000, // 每1秒一个块
ondataavailable: (blob) => {
this.chunks.push({
blob: blob,
timestamp: Date.now()
});
// 清理超过缓冲时长的旧数据块
this._cleanupOldChunks();
}
});
this.recorder.startRecording();
}
_cleanupOldChunks() {
const now = Date.now();
while (this.chunks.length > 0 &&
(now - this.chunks[0].timestamp) > this.bufferDuration) {
this.chunks.shift(); // 移除最旧的数据块
}
}
// 触发回放:将缓冲区的所有数据块合并并播放
triggerReplay() {
if (this.chunks.length === 0) {
alert('缓冲区内暂无数据。');
return;
}
// 创建一个新的Blob,包含所有数据块
const superBlob = new Blob(this.chunks.map(c => c.blob), { type: 'video/webm' });
const replayURL = URL.createObjectURL(superBlob);
const replayVideo = document.createElement('video');
replayVideo.controls = true;
replayVideo.src = replayURL;
replayVideo.style.width = '320px';
replayVideo.style.position = 'fixed';
replayVideo.style.bottom = '10px';
replayVideo.style.right = '10px';
replayVideo.style.zIndex = '1000';
document.body.appendChild(replayVideo);
replayVideo.play();
// 播放结束后清理DOM和URL
replayVideo.onended = function() {
document.body.removeChild(replayVideo);
URL.revokeObjectURL(replayURL);
};
}
stop() {
if (this.recorder) {
this.recorder.stopRecording();
this.recorder.destroy();
this.recorder = null;
this.chunks = [];
}
}
}
// 使用示例
// const buffer = new ReplayBuffer(15000); // 15秒缓冲
// buffer.start(localStream);
// 当点击“回放”按钮时:buffer.triggerReplay();
这个即时回放功能虽然简单,但非常实用,它能极大地提升在线互动应用的体验感。
4. 避坑指南与性能优化
在实际项目中使用 RecordRTC,我遇到过不少问题。这里分享几个最常见的“坑”和解决办法,希望能帮你节省时间。
4.1 兼容性与MIME类型选择
不是所有浏览器都支持所有视频编码。mimeType 配置不当会导致录制失败或无法播放。
- Chrome/Firefox/Edge (新版):通常支持
video/webm;codecs=vp9(高画质) 或vp8(高兼容)。 - Safari:Safari 对 WebM 格式支持较弱,它更倾向于 MP4 (
video/mp4)。但 RecordRTC 在 Safari 上录制 MP4 可能需要额外的配置或 polyfill。
建议做法: 进行能力检测,或者提供一个备选方案。
function getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4;codecs=avc1.42E01E',
'video/mp4'
];
for (let type of types) {
if (MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(type)) {
console.log('选择MIME类型:', type);
return type;
}
}
return 'video/webm'; // 默认回退
}
const recorder = RecordRTC(stream, {
type: 'video',
mimeType: getSupportedMimeType()
});
4.2 内存管理与大文件录制
长时间录制(如一堂2小时的课)会产生巨大的 Blob 对象,可能耗尽内存。timeSlice 是你的救星。
- 策略一:分片上传。设置
timeSlice: 10000(10秒),在ondataavailable中将每个分片立即上传到服务器,然后在服务器端拼接。这样客户端几乎不占用额外内存。 - 策略二:分片存储,最后合并。如果必须在客户端生成最终文件,可以将分片存储在
IndexedDB中,录制结束时再从数据库读出并合并。RecordRTC 本身也支持recorder.save()方法,可以将数据保存到 IndexedDB。
// 示例:使用 timeSlice 分片并模拟上传
recorder = RecordRTC(stream, {
type: 'video',
timeSlice: 10000, // 10秒一片
ondataavailable: async function(blob) {
// 为每个分片生成唯一ID
const chunkId = `chunk_${Date.now()}_${Math.random()}`;
// 1. 可选:先存到IndexedDB做备份
// await saveChunkToDB(chunkId, blob);
// 2. 上传到服务器
const formData = new FormData();
formData.append('chunk', blob);
formData.append('chunkId', chunkId);
formData.append('sessionId', recordingSessionId);
try {
await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
console.log(`分片 ${chunkId} 上传成功`);
} catch (error) {
console.error(`分片 ${chunkId} 上传失败:`, error);
// 可以考虑重试逻辑
}
}
});
4.3 音频录制问题:静音、杂音与不同步
音频问题非常常见。如果发现录制的视频没声音,或者有刺耳的杂音,可以按以下步骤排查:
- 检查权限:确保
getUserMedia的constraints中audio: true,并且用户已经允许了麦克风权限。 - 检查流中的音频轨道:
console.log(stream.getAudioTracks()),看看数组是否为空。 - 使用正确的录制器类型:对于纯音频录制,可以指定
recorderType: StereoAudioRecorder,并设置desiredSampRate(如 44100)来获得更好的音质。 - 音画不同步:这通常是由于录制过程中 CPU 负载过高或时间戳处理不当引起的。确保
timeSlice不要太短(避免高频回调),并尽量在requestAnimationFrame或 Web Worker 中进行数据处理。如果使用分片上传,服务器端拼接时需要严格按照时间戳顺序。
4.4 在框架(Vue/React)中的集成
在现代前端项目中使用 RecordRTC,关键在于生命周期管理:在组件卸载时,必须清理录制器和媒体流。
以 React 函数组件为例:
import React, { useRef, useState } from 'react';
import RecordRTC from 'recordrtc';
function RecordingComponent() {
const [recording, setRecording] = useState(false);
const recorderRef = useRef(null);
const streamRef = useRef(null);
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
streamRef.current = stream;
const recorder = RecordRTC(stream, { type: 'video' });
recorderRef.current = recorder;
recorder.startRecording();
setRecording(true);
} catch (err) {
console.error(err);
}
};
const stopRecording = () => {
if (recorderRef.current) {
recorderRef.current.stopRecording(() => {
const blob = recorderRef.current.getBlob();
// 处理blob...
// 清理
recorderRef.current.destroy();
recorderRef.current = null;
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setRecording(false);
});
}
};
// 组件卸载时清理
React.useEffect(() => {
return () => {
if (recorderRef.current) {
recorderRef.current.destroy();
}
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
};
}, []);
return (
<div>
<button onClick={startRecording} disabled={recording}>开始</button>
<button onClick={stopRecording} disabled={!recording}>停止</button>
</div>
);
}
关键点: 使用 useRef 来存储可变的录制器和流对象,避免被 React 渲染循环重置。在 useEffect 的清理函数中执行销毁操作,这是防止内存泄漏的标准做法。
5. 从录制到生产:实用技巧与扩展思路
当你掌握了基础录制和回放后,可以思考如何让这个功能变得更专业、更贴合业务。这里分享几个我项目中用到的扩展思路。
5.1 录制状态监控与UI反馈
用户需要知道录制是否正常进行。我们可以监控录制器的状态和数据。
// 扩展录制器,增加心跳监测
recorder = RecordRTC(stream, {
type: 'video',
timeSlice: 3000, // 每3秒检查一次
ondataavailable: function(blob) {
if (blob.size < 100) { // 如果连续几个分片都非常小
console.warn('录制数据异常,可能摄像头已断开?');
// 更新UI:显示警告图标
document.getElementById('recordingStatus').textContent = '⚠️ 录制信号弱';
} else {
document.getElementById('recordingStatus').textContent = '● 正在录制';
}
// 更新已录制时间
const duration = recorder.getDuration();
document.getElementById('duration').textContent = formatTime(duration);
}
});
// 计算并格式化时间
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
5.2 结合Canvas录制与特效
有时你需要录制的不只是原始视频流,而是叠加了文字、绘图、滤镜或动画的界面。这时,你可以使用 CanvasCaptureMediaStream。
// 1. 获取Canvas的2D上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// ... 在canvas上绘制你的内容(视频、图形、文字等)
// 2. 从Canvas获取媒体流
const canvasStream = canvas.captureStream(30); // 30帧/秒
// 3. 如果需要同时录制来自麦克风的音频
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 4. 将音频轨道添加到Canvas流中
audioStream.getAudioTracks().forEach(track => {
canvasStream.addTrack(track);
});
// 5. 用RecordRTC录制这个混合流
const recorder = RecordRTC(canvasStream, { type: 'video' });
recorder.startRecording();
这个技巧非常适合录制游戏画面、电子白板教学、或者带有实时标注的视频讲解。
5.3 服务器端处理与转码
客户端录制生成的是 WebM 文件。虽然现代浏览器都能播放,但你可能需要 MP4 格式以更好地兼容移动设备或编辑软件。这需要在服务器端进行转码。
一个简单的 Node.js 后端接收思路:
// Node.js (Express) 示例 - 接收分片并合并
const express = require('express');
const fs = require('fs').promises;
const app = express();
app.use(express.raw({ type: 'video/webm', limit: '500mb' })); // 接收原始二进制
app.post('/upload-recording', async (req, res) => {
const sessionId = req.headers['x-session-id'];
const chunkIndex = req.headers['x-chunk-index'];
const chunkBuffer = req.body;
const chunkPath = `./recordings/${sessionId}_chunk_${chunkIndex}.webm`;
await fs.writeFile(chunkPath, chunkBuffer);
// 当收到最后一个分片时,触发合并
if (req.headers['x-is-last'] === 'true') {
await mergeChunks(sessionId, totalChunks);
// 可以在这里调用 FFmpeg 将 final.webm 转码为 final.mp4
// exec(`ffmpeg -i final.webm -c:v libx264 -preset fast -c:a aac final.mp4`);
}
res.sendStatus(200);
});
关于转码: FFmpeg 是行业标准工具。你可以在服务器上安装 FFmpeg,使用 Node.js 的 child_process 模块来调用它,将接收到的 WebM 文件批量或实时地转码为 MP4。记得处理好队列,避免服务器过载。
5.4 录制质量控制参数调优
RecordRTC 的配置项可以显著影响输出文件的大小和质量。
const recorder = RecordRTC(stream, {
type: 'video',
// 视频比特率 (bits per second)。越高画质越好,文件越大。
bitsPerSecond: 2500000, // 2.5 Mbps,适合高清录制
// 音频比特率
audioBitsPerSecond: 128000, // 128 kbps
// 帧率(如果录制器支持)
frameRate: 30,
// 视频宽度/高度(如果录制器支持)
width: 1280,
height: 720,
// 编码质量(0到1,仅对某些编码器有效)
quality: 0.92
});
调整这些参数需要在清晰度和文件大小/网络带宽之间做权衡。对于视频会议,bitsPerSecond: 1000000 (1 Mbps) 通常就能提供不错的 720p 画质。对于需要存档的高清教学视频,可以提高到 2.5 或 3 Mbps。
最后,RecordRTC 的文档和社区其实非常活跃,遇到奇怪的问题,去 GitHub 的 Issues 里搜一搜,很大概率能找到答案。客户端录制技术正在快速发展,它为我们实现丰富、实时的 Web 应用提供了强大的工具。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)