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

如何从浏览器直接上传20秒以上至3分钟的音频文件到AWS S3?

解决浏览器端通过预签名URL上传长音频到AWS S3的问题

当前方案仅支持25秒以内音频,超过时长上传失败,核心原因是大文件直接PUT上传容易出现网络超时、中断等问题,浏览器环境下大文件单请求稳定性远低于分块上传。以下是针对性修改方案:

一、基础配置检查与优化

  1. 确认S3 Bucket的CORS配置
    确保Bucket的CORS规则允许PUT请求,且未限制文件大小:
<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>你的前端域名</AllowedOrigin> <!-- 避免使用*,提升安全性 -->
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>
  1. 优化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 08:50:23