Skip to content

音频录制

导航目录

浏览器音频录制模块封装

在项目中我封装过一个浏览器端的音频录制模块,主要用于用户留言或语音输入场景,技术上是基于 MediaRecorder 和 Vue 3 的组合式 API 来实现的,整体采用了工厂函数的形式封装,便于多处复用和状态隔离。

当调用这个模块的工厂函数时,会生成一个包含录音控制方法、播放、上传、时长计算等功能的实例对象。使用者可以直接调用这些方法来控制录音全流程,状态数据也是响应式的,方便在组件中直接绑定 UI。

功能点

  • 开始录音:会清空上次的音频缓存,设置为录音中状态,并启动 MediaRecorder 进行录音,同时开启一个定时器,每秒计算一次录音时长,并转换成分钟:秒格式展示出来。

  • 录音时长控制:支持传入最大录音时长参数,内部会自动对时长做限制,一旦超过时间会自动调用停止录音,防止过长录音。

  • 结束录音:停止 MediaRecorder 的同时清除定时器,更新状态,保存录音片段数据。

  • 上传功能:使用 axios 将录音片段合并成一个 Blob 文件,通过 FormData 上传到后端接口,后端返回音频 URL 后会保存下来用于播放或展示。

  • 播放功能:基于 Blob 创建临时 URL,通过原生 Audio 类实现预览播放功能,用户可在上传前先试听录音。


技术实现

  • 所有录音过程中的状态,包括是否正在录音、录音时长、Blob 数据队列、音频 URL 等,都是使用 Vue 的 ref 来实现响应式管理,组件中可以无缝绑定。

  • 模块本身是一个 async 工厂函数,在调用时会异步请求麦克风权限,只在真正需要的时候才初始化资源,避免在页面加载时过早弹出权限弹窗。

  • 在录音结束后,通过 Blob 对象来合并音频片段,不依赖第三方库就能完成录音上传、预览等核心功能。


模块亮点:

  • 模块结构清晰、职责分离,每个功能都通过独立的方法封装,如 startRecord()stopRecord()uploadAudio() 等,便于维护和调用。

  • 使用了懒初始化响应式状态管理结合的方式,提升了性能和使用体验。

  • 实现了自动录音时长监控,能在达到最大录音时间时自动处理逻辑,提升用户友好度。

  • 可轻松扩展到更多场景,如语音留言、语音识别前处理等。


后续优化方向

  • 支持更多编码格式,如接入 lamejs 转 MP3,提高兼容性;
  • 支持波形可视化,增强用户体验;
  • 抽离出为通用插件,支持 Vue 以外的框架;
  • 引入节流防抖机制,避免重复 start/stop 触发冲突;
  • 增加错误处理逻辑,比如权限拒绝时的提示,MediaRecorder 不支持的浏览器降级方案等。

代码实现

ts
import axios from "axios";
import { ref, Ref } from "vue";

export interface InstanceRecord {
  startRecord: () => void;
  stopRecord: () => void;
  uploadAudio: () => Promise<void>;
  playAudioStream: () => void;
  audioStream: Ref<Blob[]>;
  audioUrl: Ref<string>;
  checkRecordedAudio: Ref<boolean>;
  durationStr: Ref<string>;
  duration: Ref<number>;
}

function formatTime(seconds: number): string {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
}

export default async function useRecord(
  maxDuration = 60
): Promise<InstanceRecord> {
  const audioStream = ref<Blob[]>([]);
  const audioUrl = ref("");
  const checkRecordedAudio = ref(false);
  const durationStr = ref("00:00");
  const duration = ref(0);

  let timer: number | undefined;
  let startTime = 0;

  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const mediaRecorder = new MediaRecorder(stream);

  const updateDuration = () => {
    const now = Date.now();
    const seconds = (now - startTime) / 1000;
    duration.value = seconds;
    durationStr.value = formatTime(seconds);
    if (seconds >= maxDuration) stopRecord();
  };

  const clearTimer = () => {
    if (timer) clearInterval(timer);
    timer = undefined;
  };

  mediaRecorder.addEventListener("dataavailable", (e) => {
    if (e.data.size > 0) {
      audioStream.value.push(e.data);
    }
  });

  mediaRecorder.addEventListener("stop", () => {
    checkRecordedAudio.value = false;
    updateDuration();
    clearTimer();
  });

  const startRecord = () => {
    if (mediaRecorder.state === "recording") return;
    audioStream.value = [];
    duration.value = 0;
    durationStr.value = "00:00";
    startTime = Date.now();
    checkRecordedAudio.value = true;
    mediaRecorder.start();
    timer = window.setInterval(updateDuration, 1000);
  };

  const stopRecord = () => {
    if (mediaRecorder.state !== "recording") return;
    mediaRecorder.stop();
  };

  const uploadAudio = async () => {
    try {
      const blob = new Blob(audioStream.value, {
        type: "audio/ogg; codecs=opus",
      });
      const formData = new FormData();
      formData.append("file", blob);
      const { data } = await axios.post(
        "http://localhost:3000/upload/record",
        formData,
        {
          headers: { "Content-Type": "multipart/form-data" },
        }
      );
      audioUrl.value = data.url;
    } catch (error) {
      console.error("音频上传失败:", error);
    }
  };

  const playAudioStream = () => {
    const blob = new Blob(audioStream.value, {
      type: "audio/ogg; codecs=opus",
    });
    const audio = new Audio(URL.createObjectURL(blob));
    audio.play();
  };

  return {
    startRecord,
    stopRecord,
    uploadAudio,
    playAudioStream,
    audioStream,
    audioUrl,
    checkRecordedAudio,
    durationStr,
    duration,
  };
}