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

如何通过HTML5经WebSocket传输音视频流并重建?WebRTC与MediaRecorder存问题

基于WebSocket的HTML5音视频流传输与重建方案

针对你的需求,这里提供一个通过WebSocket中转、使用原生Media Encoder/Decoder API实现的音视频流传输方案,可解决WebRTC的隐私暴露问题,以及MediaRecorder的卡顿、延迟和丢包依赖问题。

核心思路

放弃WebRTC的P2P模式,改用WebSocket通过服务器中转音视频数据(客户端仅与服务器建立连接,无需暴露彼此公网IP);同时绕过MediaRecorder的分段机制,直接对MediaStream的音视频轨道进行实时帧编码,逐帧发送编码后数据,接收端实时解码重建流。

具体实现步骤

1. 音视频采集

使用getUserMedia获取摄像头和麦克风的MediaStream,同时支持本地预览:

async function getMediaStream() {
  const constraints = {
    video: { width: 1280, height: 720, frameRate: 30 },
    audio: { sampleRate: 48000, channelCount: 2 }
  };
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    document.getElementById('local-preview').srcObject = stream;
    return stream;
  } catch (err) {
    console.error('音视频采集失败:', err);
  }
}

2. 发送端:实时编码音视频帧

使用浏览器原生的VideoEncoder和AudioEncoder对音视频轨道的每一帧进行编码,直接输出编码后的块数据,避免MediaRecorder的分段问题:

视频编码与发送

async function setupVideoEncoder(stream, ws) {
  const videoTrack = stream.getVideoTracks()[0];
  const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
  const reader = videoProcessor.readable.getReader();

  const videoEncoder = new VideoEncoder({
    output: (chunk) => {
      // 将编码块序列化为可传输格式,携带类型和时间戳
      const payload = {
        type: 'video',
        timestamp: chunk.timestamp,
        chunkType: chunk.type,
        data: new Uint8Array(chunk.data)
      };
      // 以二进制形式发送(比JSON高效)
      ws.send(serializePayload(payload));
    },
    error: (err) => console.error('视频编码错误:', err)
  });

  // 配置H.264编码参数
  await videoEncoder.configure({
    codec: 'avc1.42E01E',
    width: 1280,
    height: 720,
    bitrate: 2_000_000, // 2Mbps,可根据网络调整
    framerate: 30,
    latencyMode: 'low-latency' // 低延迟模式
  });

  // 循环读取并编码视频帧
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    videoEncoder.encode(value);
    value.close();
  }
}

音频编码与发送

async function setupAudioEncoder(stream, ws) {
  const audioTrack = stream.getAudioTracks()[0];
  const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
  const reader = audioProcessor.readable.getReader();

  const audioEncoder = new AudioEncoder({
    output: (chunk) => {
      const payload = {
        type: 'audio',
        timestamp: chunk.timestamp,
        chunkType: chunk.type,
        data: new Uint8Array(chunk.data)
      };
      ws.send(serializePayload(payload));
    },
    error: (err) => console.error('音频编码错误:', err)
  });

  // 配置Opus编码(适合实时音频传输)
  await audioEncoder.configure({
    codec: 'opus',
    sampleRate: 48000,
    numberOfChannels: 2,
    bitrate: 128000, // 128kbps
    latencyMode: 'low-latency'
  });

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    audioEncoder.encode(value);
    value.close();
  }
}

WebSocket连接与序列化工具

// 建立WebSocket连接(服务器仅做中转,无需处理音视频数据)
function createWebSocket() {
  const ws = new WebSocket('wss://your-server-domain/stream-relay');
  
  ws.onclose = () => {
    console.log('连接断开,3秒后重连');
    setTimeout(createWebSocket, 3000);
  };
  
  ws.onerror = (err) => console.error('WebSocket错误:', err);
  
  return ws;
}

// 序列化payload为ArrayBuffer(比JSON.stringify更高效)
function serializePayload(payload) {
  // 简单序列化示例,可根据需求优化
  const encoder = new TextEncoder();
  const meta = encoder.encode(JSON.stringify({
    type: payload.type,
    timestamp: payload.timestamp,
    chunkType: payload.chunkType
  }));
  const data = payload.data;
  
  // 先发送元数据长度,再发送元数据,最后发送二进制数据
  const buffer = new ArrayBuffer(4 + meta.length + data.length);
  const view = new DataView(buffer);
  view.setUint32(0, meta.length, true);
  
  new Uint8Array(buffer, 4).set(meta);
  new Uint8Array(buffer, 4 + meta.length).set(data);
  
  return buffer;
}

3. 接收端:解码与流重建

接收WebSocket传输的编码块,使用VideoDecoder和AudioDecoder解码,再通过MediaStreamTrackGenerator生成音视频轨道,合并为MediaStream后播放:

async function setupVideoDecoder() {
  const videoGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
  const writer = videoGenerator.writable.getWriter();

  const videoDecoder = new VideoDecoder({
    output: (frame) => {
      writer.write(frame).catch(err => console.error('写入视频帧失败:', err));
      frame.close();
    },
    error: (err) => console.error('视频解码错误:', err)
  });

  await videoDecoder.configure({
    codec: 'avc1.42E01E',
    width: 1280,
    height: 720
  });

  return { videoDecoder, videoGenerator };
}

async function setupAudioDecoder() {
  const audioGenerator = new MediaStreamTrackGenerator({ kind: 'audio' });
  const writer = audioGenerator.writable.getWriter();

  const audioDecoder = new AudioDecoder({
    output: (frame) => {
      writer.write(frame).catch(err => console.error('写入音频帧失败:', err));
      frame.close();
    },
    error: (err) => console.error('音频解码错误:', err)
  });

  await audioDecoder.configure({
    codec: 'opus',
    sampleRate: 48000,
    numberOfChannels: 2
  });

  return { audioDecoder, audioGenerator };
}

// 处理WebSocket接收的数据
async function handleWebSocketMessage(event, videoDecoder, audioDecoder) {
  const buffer = event.data;
  const view = new DataView(buffer);
  
  // 解析元数据
  const metaLength = view.getUint32(0, true);
  const metaBuffer = buffer.slice(4, 4 + metaLength);
  const meta = JSON.parse(new TextDecoder().decode(metaBuffer));
  
  // 解析二进制数据
  const dataBuffer = buffer.slice(4 + metaLength);

  if (meta.type === 'video') {
    const chunk = new EncodedVideoChunk({
      type: meta.chunkType,
      timestamp: meta.timestamp,
      data: dataBuffer
    });
    await videoDecoder.decode(chunk);
  } else if (meta.type === 'audio') {
    const chunk = new EncodedAudioChunk({
      type: meta.chunkType,
      timestamp: meta.timestamp,
      data: dataBuffer
    });
    await audioDecoder.decode(chunk);
  }
}

// 初始化接收端
async function initReceiver() {
  const { videoDecoder, videoGenerator } = await setupVideoDecoder();
  const { audioDecoder, audioGenerator } = await setupAudioDecoder();
  
  const remoteStream = new MediaStream([videoGenerator, audioGenerator]);
  document.getElementById('remote-player').srcObject = remoteStream;
  
  const ws = createWebSocket();
  ws.onmessage = (event) => handleWebSocketMessage(event, videoDecoder, audioDecoder);
}

关键问题解决

  • 隐私保护:所有数据通过WebSocket服务器中转,客户端仅与服务器建立连接,无需暴露彼此公网IP。
  • 卡顿与延迟:采用逐帧编码发送的方式,避免MediaRecorder的分段等待,同时开启低延迟编码模式,平衡延迟与质量。
  • 丢包处理:可在发送端为每个帧块添加序号,接收端检测到序号断层时,通过WebSocket向服务器请求重传对应帧块;也可在编码时加入简单的FEC(前向纠错)数据,降低丢包对播放的影响。

注意事项

  • 浏览器兼容性:Media Encoder/Decoder API目前在Chrome、Edge 94+支持良好,Firefox需开启media.webrtc.hw.h264.enabled等配置,Safari 16.4+开始支持。
  • 服务器要求:中转服务器需支持WebSocket协议,且具备足够的带宽和CPU资源处理二进制数据转发。
  • 音视频同步:依赖编码帧的时间戳,接收端解码时会自动根据时间戳对齐,无需额外处理。

内容的提问来源于stack exchange,提问作者z3db0y

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:25:02