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
相关产品推荐
相关产品推荐

