如何用Node.js+Socket.io+PeerJS实现多人视频聊天全程录制功能?
实现多参与者音视频合成录制方案
要实现类似Zoom的单文件录制所有参与者画面,核心是将所有本地/远程音视频流合并为一个合成流,再用MediaRecorder录制这个合成流。以下是具体实现步骤和代码:
核心思路
- 收集房间内所有参与者的音视频流(本地+远程)
- 用
AudioContext混合所有音频流为单轨道 - 用
Canvas将所有视频画面绘制到同一画布,导出合成视频流 - 合并合成的音视频流,用
MediaRecorder录制并导出文件
代码实现
1. 初始化全局变量
用于存储所有流、合成对象和录制数据:
// 存储所有参与者的音视频流(key: 用户唯一标识) const allStreams = new Map(); // 存储每个流对应的video元素(用于Canvas绘制) const videoElements = new Map(); // 音频合成上下文和输出 let audioContext; let audioDestination; // 视频合成画布 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // 最终合成的音视频流 let combinedStream; // 录制器实例 let mediaRecorder; // 存储录制的媒体片段 let recordedChunks = [];
2. 收集本地/远程流
在原有逻辑中加入流收集逻辑:
// 获取本地流并存储 navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then((localStream) => { // 存储本地流,用'local'作为标识 allStreams.set('local', localStream); // 创建本地视频元素 const localVideo = document.createElement('video'); localVideo.srcObject = localStream; localVideo.muted = true; // 避免本地音频回声 localVideo.play(); videoElements.set('local', localVideo); // 初始化合成流 initCombinedStream(); // 你的原有Socket.io/PeerJS逻辑(比如加入房间、展示视频) // ... }); // PeerJS获取远程流时存储 peer.on('stream', (remoteStream) => { const peerId = remoteStream.id; // 用PeerID作为用户标识 allStreams.set(peerId, remoteStream); // 创建远程视频元素 const remoteVideo = document.createElement('video'); remoteVideo.srcObject = remoteStream; remoteVideo.play(); videoElements.set(peerId, remoteVideo); // 更新合成流(适配新加入的用户) updateCombinedStream(); // 你的原有远程视频展示逻辑 // ... }); // 用户离开时移除对应流 socket.on('user-disconnected', (peerId) => { allStreams.delete(peerId); videoElements.delete(peerId); updateCombinedStream(); });
3. 合成音频流
用AudioContext将多个音频轨道混合为单轨道:
function mixAudioStreams() { audioContext = new AudioContext(); audioDestination = audioContext.createMediaStreamDestination(); // 遍历所有流,将音频源连接到混音输出 allStreams.forEach((stream) => { const audioSource = audioContext.createMediaStreamSource(stream); audioSource.connect(audioDestination); }); return audioDestination.stream.getAudioTracks()[0]; }
4. 合成视频流
用Canvas循环绘制所有参与者的视频画面:
function drawCanvas() { const videos = Array.from(videoElements.values()); const participantCount = videos.length; if (participantCount === 0) return; // 动态计算画布布局(示例:按2列排列) const cols = Math.ceil(Math.sqrt(participantCount)); const rows = Math.ceil(participantCount / cols); const singleVideoWidth = 640; const singleVideoHeight = 480; // 设置画布尺寸 canvas.width = singleVideoWidth * cols; canvas.height = singleVideoHeight * rows; // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 逐个绘制视频画面 videos.forEach((video, index) => { const x = (index % cols) * singleVideoWidth; const y = Math.floor(index / cols) * singleVideoHeight; ctx.drawImage(video, x, y, singleVideoWidth, singleVideoHeight); }); // 持续绘制以更新画面 requestAnimationFrame(drawCanvas); }
5. 初始化/更新合成流
合并合成的音视频流:
function initCombinedStream() { // 启动Canvas绘制循环 drawCanvas(); // 获取Canvas的视频流(30帧/秒) const canvasVideoStream = canvas.captureStream(30); // 获取混音后的音频轨道 const mixedAudioTrack = mixAudioStreams(); // 合并为最终合成流 combinedStream = new MediaStream([ ...canvasVideoStream.getVideoTracks(), mixedAudioTrack ]); } // 有用户加入/离开时更新合成流 function updateCombinedStream() { if (!combinedStream) return; // 关闭旧的音频上下文,重新混音 if (audioContext) audioContext.close(); const newAudioTrack = mixAudioStreams(); // 替换合成流中的音频轨道 const oldAudioTracks = combinedStream.getAudioTracks(); oldAudioTracks.forEach(track => combinedStream.removeTrack(track)); combinedStream.addTrack(newAudioTrack); }
6. 录制控制函数
实现开始/停止录制并导出文件:
// 开始录制 function startRecording() { if (!combinedStream) return; recordedChunks = []; // 设置录制格式(优先选择浏览器支持的格式) const recorderOptions = { mimeType: 'video/webm; codecs=vp9,opus' }; mediaRecorder = new MediaRecorder(combinedStream, recorderOptions); // 收集录制片段 mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) recordedChunks.push(e.data); }; // 录制结束后生成下载链接 mediaRecorder.onstop = () => { const recordingBlob = new Blob(recordedChunks, { type: 'video/webm' }); const downloadUrl = URL.createObjectURL(recordingBlob); const downloadLink = document.createElement('a'); downloadLink.href = downloadUrl; downloadLink.download = `chat-recording-${Date.now()}.webm`; downloadLink.click(); URL.revokeObjectURL(downloadUrl); }; mediaRecorder.start(); } // 停止录制 function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); } }
注意事项
- 浏览器兼容性:确保目标浏览器支持
MediaRecorder、AudioContext和Canvas.captureStream(Chrome、Firefox支持较好,Safari 14.1+支持) - 性能优化:如果房间人数较多,可降低单视频分辨率或限制最大录制帧率,避免性能瓶颈
- 音频同步:不同流的音频可能存在微小延迟,可通过
AudioContext的时间戳API微调同步 - 资源清理:用户离开时务必移除对应的流和视频元素,避免内存泄漏
内容的提问来源于stack exchange,提问作者Roy_In
相关产品推荐
相关产品推荐

