如何在浏览器中合并多个.webm音频文件为单个可下载文件
解决WebM音频分片浏览器端合并后下载播放异常的问题
直接拼接WebM分片的二进制数据会导致下载后的文件仅播放第一个分片,核心原因是每个WebM分片都包含独立的EBML容器头部,播放器解析时只会识别第一个合法的文件头,后续分片的头部会被判定为无效数据,忽略后面的媒体内容。WebM基于Matroska容器,正确的合并方式是保留第一个分片的完整头部,后续分片仅提取其中的媒体数据块(Cluster)进行拼接。
实现步骤
- 解析WebM分片,提取Cluster起始位置:WebM文件结构为EBML头 → Segment → Cluster(媒体数据),需找到每个分片里第一个Cluster的起始字节位置,后续分片只从该位置截取数据。
- 合并分片数据:保留第一个分片的完整数据(含头部和所有Cluster),后续分片仅截取Cluster起始位置到末尾的二进制数据,最后将所有处理后的二进制数据拼接为完整ArrayBuffer再转为Blob。
代码实现
// 解析WebM分片,返回第一个Cluster的起始字节偏移 async function getClusterStart(buffer) { const view = new DataView(buffer); let offset = 0; // 跳过EBML头:EBML元素ID是0x1A45DFA3,读取其大小后跳过整个EBML块 if (view.getUint32(offset) === 0x1A45DFA3) { const size = readEBMLSize(view, offset + 4); offset += 4 + size; } // 找到Segment元素(ID: 0x18538067) while (offset < view.byteLength) { const id = view.getUint32(offset); if (id === 0x18538067) { const size = readEBMLSize(view, offset + 4); offset += 4 + size; break; } const size = readEBMLSize(view, offset + 4); offset += 4 + size; } // 找到第一个Cluster元素(ID: 0x1F43B675) while (offset < view.byteLength) { const id = view.getUint32(offset); if (id === 0x1F43B675) { return offset; } const size = readEBMLSize(view, offset + 4); offset += 4 + size; } throw new Error("未找到Cluster元素"); } // 读取EBML元素的可变长度大小 function readEBMLSize(view, offset) { const firstByte = view.getUint8(offset); let lengthBits = 0; let mask = 0x80; while (!(firstByte & mask)) { mask >>= 1; lengthBits++; } const sizeBytes = lengthBits + 1; let size = firstByte & ~mask; for (let i = 1; i < sizeBytes; i++) { size = (size << 8) | view.getUint8(offset + i); } return size; } // 合并WebM分片的主函数 async function mergeWebMSegments(segmentUrls) { const mergedBuffers = []; for (let i = 0; i < segmentUrls.length; i++) { const response = await fetch(segmentUrls[i]); const buffer = await response.arrayBuffer(); if (i === 0) { // 第一个分片保留完整数据 mergedBuffers.push(buffer); } else { // 后续分片只取Cluster及之后的数据 const clusterStart = await getClusterStart(buffer); const clusterBuffer = buffer.slice(clusterStart); mergedBuffers.push(clusterBuffer); } } // 拼接所有ArrayBuffer const totalLength = mergedBuffers.reduce((sum, buf) => sum + buf.byteLength, 0); const mergedArray = new Uint8Array(totalLength); let offset = 0; mergedBuffers.forEach(buf => { mergedArray.set(new Uint8Array(buf), offset); offset += buf.byteLength; }); // 返回合并后的Blob,MIME类型设为audio/webm return new Blob([mergedArray], { type: 'audio/webm' }); } // 使用示例:替换为你的S3分片URL列表 const s3SegmentUrls = [ 'https://your-bucket.s3.amazonaws.com/segment1.webm', 'https://your-bucket.s3.amazonaws.com/segment2.webm', // ...更多分片 ]; mergeWebMSegments(s3SegmentUrls).then(mergedBlob => { // 创建下载链接 const url = URL.createObjectURL(mergedBlob); const a = document.createElement('a'); a.href = url; a.download = 'merged-audio.webm'; a.click(); URL.revokeObjectURL(url); }).catch(err => { console.error('合并失败:', err); });
关键说明
- EBML解析:WebM使用EBML(扩展二进制元语言)作为容器格式,元素ID和大小均为可变长度,
readEBMLSize函数负责解析这种可变长度的大小字段。 - Cluster提取:后续分片必须去掉头部,仅保留Cluster部分,确保合并后的文件只有一个合法的WebM头部,播放器能正确识别所有媒体数据。
- MIME类型:生成Blob时必须指定
audio/webm,保证浏览器和播放器正确识别文件格式。
内容的提问来源于stack exchange,提问作者qasim mehmood
相关产品推荐
相关产品推荐

