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 音频录制问题:静音、杂音与不同步

音频问题非常常见。如果发现录制的视频没声音,或者有刺耳的杂音,可以按以下步骤排查:

  1. 检查权限:确保 getUserMedia 的 constraints 中 audio: true,并且用户已经允许了麦克风权限。
  2. 检查流中的音频轨道:console.log(stream.getAudioTracks()),看看数组是否为空。
  3. 使用正确的录制器类型:对于纯音频录制,可以指定 recorderType: StereoAudioRecorder,并设置 desiredSampRate(如 44100)来获得更好的音质。
  4. 音画不同步:这通常是由于录制过程中 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 应用提供了强大的工具。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐