【实用案例】录音分片上传的核心逻辑和实现案例【文章附有代码】

前言

最近接到一个业务需求:录音文件上传时因为文件过大(超过100M)有报错,

(服务器端对单次文件的上传,有大小限制)。

经过讨论,最后决定使用‘分片’解决问题。

我做了一个小案例(非业务代码),用HTML代码实现前端页面用express实现后端服务

一、分片上传的核心原理​

分片上传是将大文件分割成多个小片段(分片),逐个上传到服务器,最后在服务器端合并所有分片形成完整文件的技术。这种方式的优势在于:​

  • 避免单次上传大文件导致的超时问题​
  • 支持断点续传,某分片失败只需重传该分片​
  • 减轻服务器单次处理压力,提高系统稳定性​

对于录音文件来说,由于可能包含长时间的音频数据(尤其是高清录音),文件体积往往较大,非常适合采用分片上传方案。

二、前端实现核心逻辑​

前端主要负责录音、文件分片和上传控制三个部分。

1. 录音功能实现​

使用浏览器原生的MediaRecorderAPI 实现录音功能:

// 开始录音
startRecordBtn.addEventListener('click', async () => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  mediaRecorder = new MediaRecorder(stream);
  audioChunks = [];
  mediaRecorder.ondataavailable = (event) => {
    audioChunks.push(event.data);
  };
  mediaRecorder.start();
  // 更新UI状态...
});

// 停止录音
stopRecordBtn.addEventListener('click', () => {
  mediaRecorder.stop();
  mediaRecorder.stream.getTracks().forEach(track => track.stop());
  mediaRecorder.onstop = () => {
    audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
    audioUrl = URL.createObjectURL(audioBlob);
    // 显示上传区域...
  };
});

2. 分片上传核心实现​

创建AudioChunkUploader类处理分片逻辑:

class AudioChunkUploader {
  constructor(options) {
    this.options = {
      chunkSize: 5 * 1024 * 1024, // 5MB每片
      uploadUrl: 'http://localhost:3000/upload/chunk',
      mergeUrl: 'http://localhost:3000/upload/merge',
      ...options
    };
    this.state = {
      totalChunks: 0,
      uploadedChunks: 0,
      fileMd5: '', // 文件唯一标识
      isPaused: false
    };
  }

  // 计算文件MD5(用于标识同一文件)
  async _calculateFileMd5() {
    return new Promise((resolve) => {
      const fileReader = new FileReader();
      const spark = new SparkMD5.ArrayBuffer();
      fileReader.onload = (e) => {
        spark.append(e.target.result);
        this.state.fileMd5 = spark.end();
        resolve();
      };
      fileReader.readAsArrayBuffer(this.options.file);
    });
  }

  // 上传单个分片
  async _uploadSingleChunk(chunkIndex) {
    const start = chunkIndex * this.options.chunkSize;
    const end = Math.min(start + this.options.chunkSize, this.state.fileSize);
    const chunk = this.options.file.slice(start, end);
    
    const formData = new FormData();
    formData.append('chunk', chunkIndex);
    formData.append('chunks', this.state.totalChunks);
    formData.append('fileMd5', this.state.fileMd5);
    formData.append('file', chunk);
    
    return fetch(this.options.uploadUrl, {
      method: 'POST',
      body: formData
    }).then(response => response.json());
  }

  // 上传所有分片
  async _uploadChunks() {
    for (let i = 0; i < this.state.totalChunks; i++) {
      if (this.state.isPaused) {
        // 等待恢复上传
        await new Promise(resolve => {
          const check = setInterval(() => {
            if (!this.state.isPaused) {
              clearInterval(check);
              resolve();
            }
          }, 100);
        });
      }
      await this._uploadSingleChunk(i);
      this.state.uploadedChunks = i + 1;
      // 触发进度更新...
    }
  }

  // 通知服务器合并分片
  async _mergeChunks() {
    return fetch(this.options.mergeUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileMd5: this.state.fileMd5,
        fileName: this.options.fileName,
        chunks: this.state.totalChunks
      })
    }).then(response => response.json());
  }
}

3. 播放控制功能​

实现带暂停 / 继续功能的音频播放:

let audioPlayer = null;
let lastPlayTime = 0;

// 播放录音
playRecordBtn.addEventListener('click', () => {
  if (!audioPlayer) {
    audioPlayer = new Audio(audioUrl);
    if (lastPlayTime > 0) {
      audioPlayer.currentTime = lastPlayTime;
    }
  } else {
    audioPlayer.currentTime = lastPlayTime;
  }
  audioPlayer.play();
  // 更新按钮状态...
});

// 暂停播放
stopPlayBtn.addEventListener('click', () => {
  lastPlayTime = audioPlayer.currentTime;
  audioPlayer.pause();
  // 更新按钮状态...
});

三、后端实现核心逻辑​

Express 后端负责接收分片、临时存储和合并文件:​

1. 分片接收接口

const multer = require('multer');
const upload = multer({ 
  storage: multer.diskStorage({
    destination: (req, file, cb) => {
      // 为每个文件创建独立的临时目录
      const chunkDir = path.join(tempDir, req.body.fileMd5);
      if (!fs.existsSync(chunkDir)) {
        fs.mkdirSync(chunkDir, { recursive: true });
      }
      cb(null, chunkDir);
    },
    filename: (req, file, cb) => {
      // 用分片索引作为文件名
      cb(null, req.body.chunk);
    }
  })
});

// 处理分片上传
app.post('/upload/chunk', upload.single('file'), (req, res) => {
  res.json({
    success: true,
    message: `分片 ${req.body.chunk} 上传成功`
  });
});

2. 分片合并接口

app.post('/upload/merge', (req, res) => {
  const { fileMd5, fileName, chunks } = req.body;
  const chunkDir = path.join(tempDir, fileMd5);
  const destPath = path.join(uploadDir, fileName);
  
  // 检查所有分片是否上传完成
  if (fs.readdirSync(chunkDir).length !== parseInt(chunks)) {
    return res.status(400).json({
      success: false,
      message: '分片不完整'
    });
  }
  
  // 合并分片
  const writeStream = fs.createWriteStream(destPath);
  let chunkIndex = 0;
  
  const mergeNextChunk = () => {
    const chunkPath = path.join(chunkDir, chunkIndex.toString());
    if (fs.existsSync(chunkPath)) {
      const readStream = fs.createReadStream(chunkPath);
      readStream.pipe(writeStream, { end: false });
      readStream.on('end', () => {
        fs.unlinkSync(chunkPath); // 删除已合并的分片
        chunkIndex++;
        mergeNextChunk();
      });
    } else {
      writeStream.end(); // 所有分片合并完成
    }
  };
  
  mergeNextChunk();
  
  writeStream.on('finish', () => {
    fs.rmdirSync(chunkDir, { recursive: true }); // 清理临时目录
    res.json({
      success: true,
      fileName,
      filePath: destPath
    });
  });
});

四、效果展示

可以在设定好存放上传文件的文件夹下看到上传的录音文件

五、总结与拓展​

录音文件分片上传方案的核心在于:​

  • 前端将录音文件分割成固定大小的分片,计算文件唯一标识​
  • 逐个上传分片,支持暂停 / 继续功能​
  • 后端接收分片并临时存储​
  • 所有分片上传完成后,后端按顺序合并成完整文件​

方案可以进一步优化:​

  • 添加断点续传功能,上传前先查询已上传的分片​
  • 实现分片上传的并发控制,提高上传速度​
  • 增加文件校验机制,确保上传文件的完整性​
  • 对大文件 MD5 计算进行优化,避免页面卡顿​

分片上传技术不仅适用于录音文件,也可推广到视频、文档等各种大文件上传场景,是 Web 开发中处理大文件的重要方案。

代码获取,后台联系。

评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十八朵郁金香

感恩前行路上有你相伴

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值