You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Node.js+Socket.io+PeerJS实现多人视频聊天全程录制功能?

实现多参与者音视频合成录制方案

要实现类似Zoom的单文件录制所有参与者画面,核心是将所有本地/远程音视频流合并为一个合成流,再用MediaRecorder录制这个合成流。以下是具体实现步骤和代码:

核心思路

  1. 收集房间内所有参与者的音视频流(本地+远程)
  2. 用AudioContext混合所有音频流为单轨道
  3. 用Canvas将所有视频画面绘制到同一画布,导出合成视频流
  4. 合并合成的音视频流,用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 01:42:22