如何从浏览器直接上传20秒以上至3分钟的音频文件到AWS S3?
解决浏览器端通过预签名URL上传长音频到AWS S3的问题
当前方案仅支持25秒以内音频,超过时长上传失败,核心原因是大文件直接PUT上传容易出现网络超时、中断等问题,浏览器环境下大文件单请求稳定性远低于分块上传。以下是针对性修改方案:
一、基础配置检查与优化
- 确认S3 Bucket的CORS配置
确保Bucket的CORS规则允许PUT请求,且未限制文件大小:
<CORSConfiguration> <CORSRule> <AllowedOrigin>你的前端域名</AllowedOrigin> <!-- 避免使用*,提升安全性 --> <AllowedMethod>PUT</AllowedMethod> <AllowedHeader>*</AllowedHeader> <MaxAgeSeconds>3000</MaxAgeSeconds> </CORSRule> </CORSConfiguration>
- 优化XHR上传基础配置
给XHR添加超时设置和进度监听,便于排查上传失败原因:
function uploadFile(file, signedRequest, url) { const xhr = new XMLHttpRequest(); xhr.open('PUT', signedRequest); xhr.timeout = 300000; // 设置5分钟超时 xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = (event.loaded / event.total) * 100; console.log(`上传进度:${Math.round(percent)}%`); } }; xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { console.log('上传成功,文件地址:', url); } else { console.error('上传失败,状态码:', xhr.status, '响应:', xhr.responseText); alert('文件上传失败,请重试'); } } }; xhr.ontimeout = () => { console.error('上传超时'); alert('上传超时,请重试'); }; xhr.send(file); }
二、实现分块上传(核心修改)
对于3分钟左右的音频(约3-5MB),采用S3的Multipart Upload分块上传,大幅提升大文件上传稳定性:
1. Node.js服务新增分块上传相关接口
需要新增三个接口:初始化分块上传、获取块预签名URL、完成分块上传
const aws = require('aws-sdk'); aws.config.region = 'xxxxxx'; const S3_BUCKET = process.env.AWS_S3_BUCKET; const s3 = new aws.S3(); // 初始化分块上传 this.initMultipartUpload = async function(req, res) { const subFolderName = req.query['fileLoc']; const fileName = req.query['file-name']; const fileType = req.query['file-type']; const fileLocName = subFolderName + fileName; const params = { Bucket: S3_BUCKET, Key: fileLocName, ContentType: fileType, ACL: 'public-read' }; try { const result = await s3.createMultipartUpload(params).promise(); res.json({ uploadId: result.UploadId, key: fileLocName, url: `https://${S3_BUCKET}.s3.amazonaws.com/${fileLocName}` }); } catch (err) { console.error('初始化分块上传失败:', err); res.status(500).json({ error: err.message }); } }; // 获取单个块的预签名URL this.getPartSignedUrl = async function(req, res) { const key = req.query['key']; const uploadId = req.query['uploadId']; const partNumber = req.query['partNumber']; const URL_EXPIRATION_SECONDS = 3000; const params = { Bucket: S3_BUCKET, Key: key, UploadId: uploadId, PartNumber: partNumber, Expires: URL_EXPIRATION_SECONDS }; try { const signedUrl = await s3.getSignedUrlPromise('uploadPart', params); res.json({ signedRequest: signedUrl }); } catch (err) { console.error('获取块签名URL失败:', err); res.status(500).json({ error: err.message }); } }; // 完成分块上传 this.completeMultipartUpload = async function(req, res) { const key = req.body.key; const uploadId = req.body.uploadId; const parts = req.body.parts; const params = { Bucket: S3_BUCKET, Key: key, UploadId: uploadId, MultipartUpload: { Parts: parts } }; try { const result = await s3.completeMultipartUpload(params).promise(); res.json({ url: result.Location }); } catch (err) { console.error('完成分块上传失败:', err); res.status(500).json({ error: err.message }); } };
2. 浏览器端修改上传逻辑,实现分块上传
// 分块大小设置为5MB(可根据网络情况调整) const CHUNK_SIZE = 5 * 1024 * 1024; async function uploadLargeAudio(file, fileLoc) { // 1. 初始化分块上传 const initUrl = `/init-multipart?file-name=${file.name}&file-type=${file.type}&fileLoc=${fileLoc}`; const initResponse = await fetch(initUrl); if (!initResponse.ok) throw new Error('初始化分块上传失败'); const { uploadId, key, url } = await initResponse.json(); // 2. 分割文件并上传每个块 const chunkCount = Math.ceil(file.size / CHUNK_SIZE); const parts = []; for (let i = 0; i < chunkCount; i++) { const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); // 获取块的预签名URL const partUrl = `/get-part-signed-url?key=${encodeURIComponent(key)}&uploadId=${uploadId}&partNumber=${i + 1}`; const partResponse = await fetch(partUrl); if (!partResponse.ok) throw new Error(`获取第${i+1}块签名失败`); const { signedRequest } = await partResponse.json(); // 上传块 const uploadResponse = await uploadChunk(chunk, signedRequest); if (uploadResponse.status !== 200) throw new Error(`第${i+1}块上传失败`); // 记录块的ETag const eTag = uploadResponse.headers.get('ETag'); parts.push({ PartNumber: i + 1, ETag: eTag.replace(/"/g, '') }); } // 3. 完成分块上传 const completeUrl = `/complete-multipart`; const completeResponse = await fetch(completeUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, uploadId, parts }) }); if (completeResponse.ok) { console.log('上传成功,文件地址:', url); } else { throw new Error('完成分块上传失败'); } } // 上传单个块的辅助函数 function uploadChunk(chunk, signedRequest) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', signedRequest); xhr.timeout = 60000; // 单个块上传超时设为1分钟 xhr.onload = () => resolve(xhr); xhr.onerror = () => reject(new Error('块上传网络错误')); xhr.ontimeout = () => reject(new Error('块上传超时')); xhr.send(chunk); }); } // 替换原来的getFetchSignedRequest调用 // getSignedRequest(file,fileLoc); uploadLargeAudio(file, fileLoc);
三、额外注意事项
- 音频编码优化:配置recorder.js生成MP3格式音频(比WAV体积小70%以上),减少文件大小,进一步提升上传稳定性。
- 错误重试机制:给块上传添加1-2次自动重试逻辑,应对临时网络波动。
- 大小分支处理:前端可判断文件大小,小于5MB时用原单文件上传,大于时用分块上传,兼顾效率与稳定性。
内容的提问来源于stack exchange,提问作者sapjjr
相关产品推荐
相关产品推荐

