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

WebRTC传输含ArrayBuffer的对象时数据丢失问题求解

解决方案:WebRTC传输带ArrayBuffer的文件块并保留元数据

问题核心是JSON无法直接序列化ArrayBuffer,JSON.stringify处理时会将其转为空对象,导致数据丢失。以下是两种可落地的解决方案,均支持多文件同时传输:


方法一:Base64编码传输(简单易实现)

将ArrayBuffer转为Base64字符串后嵌入JSON对象,接收端再解码回ArrayBuffer。缺点是Base64会让数据体积增加约33%,因此需要调整原块大小(比如将原块设为48KB,转Base64后约64KB,符合WebRTC的上限)。

发送端代码

// ArrayBuffer转Base64
function arrayBufferToBase64(buffer) {
  const byteArray = new Uint8Array(buffer);
  let binaryStr = '';
  for (const byte of byteArray) {
    binaryStr += String.fromCharCode(byte);
  }
  return btoa(binaryStr);
}

// 构造包含fileId的块数据包
const chunkPacket = {
  fileId: 'unique-file-uuid-123',
  chunkIndex: 0,
  isLastChunk: false,
  chunkData: arrayBufferToBase64(chunkData) // 转Base64后嵌入
};

// 通过DataChannel发送
dataChannel.send(JSON.stringify(chunkPacket));

接收端代码

// Base64转ArrayBuffer
function base64ToArrayBuffer(base64Str) {
  const binaryStr = atob(base64Str);
  const byteArray = new Uint8Array(binaryStr.length);
  for (let i = 0; i < binaryStr.length; i++) {
    byteArray[i] = binaryStr.charCodeAt(i);
  }
  return byteArray.buffer;
}

dataChannel.onmessage = (event) => {
  const packet = JSON.parse(event.data);
  if (packet.chunkData) {
    // 解码回ArrayBuffer
    const chunkData = base64ToArrayBuffer(packet.chunkData);
    // 根据fileId分配到对应文件的接收队列
    handleChunk(packet.fileId, chunkData, packet.chunkIndex, packet.isLastChunk);
  } else if (packet.fileId && packet.fileName) {
    // 处理元数据包,初始化对应文件的接收状态
    initFileReceiver(packet.fileId, packet.fileName, packet.size);
  }
};

方法二:自定义二进制格式(高效无体积膨胀)

直接构造包含元数据的二进制包,将fileId、块索引等信息放在头部,后面紧跟原始块数据。这种方法没有额外体积开销,且每个包独立携带fileId,完全支持多文件并行传输。

发送端代码

// UUID转Uint8Array(假设fileId是标准UUID)
function uuidToUint8Array(uuid) {
  const hexStr = uuid.replace(/-/g, '');
  const byteArray = new Uint8Array(16);
  for (let i = 0; i < 16; i++) {
    byteArray[i] = parseInt(hexStr.substr(i * 2, 2), 16);
  }
  return byteArray;
}

// 构造二进制数据包:[16字节fileId][4字节块索引][1字节是否最后块][块数据]
function buildBinaryChunkPacket(fileId, chunkIndex, isLastChunk, chunkData) {
  const fileIdBytes = uuidToUint8Array(fileId);
  const chunkIndexBytes = new Uint32Array([chunkIndex]).buffer;
  const isLastByte = new Uint8Array([isLastChunk ? 1 : 0]).buffer;

  // 计算总长度并创建缓冲区
  const totalLen = fileIdBytes.length + chunkIndexBytes.byteLength + isLastByte.byteLength + chunkData.byteLength;
  const combinedBuffer = new ArrayBuffer(totalLen);
  const view = new Uint8Array(combinedBuffer);

  // 复制各部分数据到缓冲区
  view.set(fileIdBytes, 0);
  view.set(new Uint8Array(chunkIndexBytes), 16);
  view.set(new Uint8Array(isLastByte), 20);
  view.set(new Uint8Array(chunkData), 21);

  return combinedBuffer;
}

// 发送二进制包
const binaryPacket = buildBinaryChunkPacket('unique-file-uuid-123', 0, false, chunkData);
dataChannel.send(binaryPacket);

// 元数据仍用JSON发送
const metaPacket = {
  fileId: 'unique-file-uuid-123',
  fileName: 'example.pdf',
  size: 1024 * 1024
};
dataChannel.send(JSON.stringify(metaPacket));

接收端代码

// Uint8Array转UUID
function uint8ArrayToUuid(byteArray) {
  const hexParts = [];
  for (const byte of byteArray) {
    hexParts.push(byte.toString(16).padStart(2, '0'));
  }
  const hexStr = hexParts.join('');
  return [
    hexStr.substr(0, 8),
    hexStr.substr(8, 4),
    hexStr.substr(12, 4),
    hexStr.substr(16, 4),
    hexStr.substr(20, 12)
  ].join('-');
}

// 解析二进制数据包
function parseBinaryChunkPacket(buffer) {
  const view = new Uint8Array(buffer);
  
  // 读取fileId(前16字节)
  const fileIdBytes = view.slice(0, 16);
  const fileId = uint8ArrayToUuid(fileIdBytes);
  
  // 读取块索引(16-20字节,Uint32)
  const chunkIndex = new Uint32Array(view.slice(16, 20).buffer)[0];
  
  // 读取是否最后块(第21字节)
  const isLastChunk = view[20] === 1;
  
  // 读取块数据(从21字节开始到结尾)
  const chunkData = view.slice(21).buffer;
  
  return { fileId, chunkIndex, isLastChunk, chunkData };
}

dataChannel.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    // 处理二进制块数据包
    const packet = parseBinaryChunkPacket(event.data);
    handleChunk(packet.fileId, packet.chunkData, packet.chunkIndex, packet.isLastChunk);
  } else {
    // 处理JSON格式的元数据包
    const meta = JSON.parse(event.data);
    initFileReceiver(meta.fileId, meta.fileName, meta.size);
  }
};

多文件传输的状态管理

接收端可以维护一个对象,以fileId为键存储每个文件的接收状态:

const fileReceivers = {};

function initFileReceiver(fileId, fileName, totalSize) {
  fileReceivers[fileId] = {
    fileName,
    totalSize,
    receivedChunks: new Map(), // 存储已接收的块索引和数据
    receivedSize: 0
  };
}

function handleChunk(fileId, chunkData, chunkIndex, isLastChunk) {
  const receiver = fileReceivers[fileId];
  if (!receiver) return;
  
  receiver.receivedChunks.set(chunkIndex, chunkData);
  receiver.receivedSize += chunkData.byteLength;
  
  // 检查是否接收完成
  if (isLastChunk && receiver.receivedSize === receiver.totalSize) {
    // 合并所有块并保存文件
    const totalBuffer = new ArrayBuffer(receiver.totalSize);
    const view = new Uint8Array(totalBuffer);
    let offset = 0;
    for (let i = 0; i <= chunkIndex; i++) {
      const chunk = receiver.receivedChunks.get(i);
      view.set(new Uint8Array(chunk), offset);
      offset += chunk.byteLength;
    }
    saveFile(new Blob([totalBuffer]), receiver.fileName);
    // 清理状态
    delete fileReceivers[fileId];
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 01:20:15