使用Web内置 API MediaDevices,完成web的录音功能

1,415 阅读5分钟

工具:MediaDevices接口提供访问连接媒体输入的设备,如照相机和麦克风,以及屏幕共享等。它可以使你取得任何硬件资源的媒体数据。

API/MediaDevices

实现功能点:在web页面中,获取音频流

开始

采用navigator.mediaDevices.getUserMedia() 获取音频流,需要考虑以下几点:

  1. 浏览器兼容性问题
  2. 权限问题
  3. 音频流格式问题

一、浏览器兼容性问题

但不同浏览器的实现存在差异 先说浏览器兼容性问题,目前主流浏览器都支持getUserMedia(),但不同浏览器的实现存在差异,需要根据不同浏览器进行适配。

image.png

主要有以下几点不同:

1. API命名差异:

  • 现代浏览器统一使用 navigator.mediaDevices.getUserMedia()
  • 旧版Chrome使用 webkitGetUserMedia()
  • 旧版Firefox使用 mozGetUserMedia()
  • 旧版Edge使用 msGetUserMedia()

2. 参数配置差异:

  • 所有浏览器都要求必须设置audio或video参数
  • Firefox支持更多音频编解码器和格式
  • Safari对某些高级音频参数支持有限

3. 错误处理差异:

  • 现代浏览器返回Promise对象
  • 旧版本使用回调函数
  • 不同浏览器的错误信息格式不统一

4. 兼容性处理方案: 一般用不上,使用标准的navigator.mediaDevices.getUserMedia()方法,对于不支持的浏览器给出明确提示,用户浏览器版本不支持即可,对就是那么硬!

// 定义一个兼容性处理函数
function getUserMediaSupport() {
  // 标准方法
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    return navigator.mediaDevices.getUserMedia;
  }
  
  // 旧版浏览器前缀方法
  const getUserMedia = navigator.getUserMedia || 
                      navigator.webkitGetUserMedia ||
                      navigator.mozGetUserMedia ||
                      navigator.msGetUserMedia;
                      
  if (!getUserMedia) {
    return Promise.reject(new Error('getUserMedia 不被支持'));
  }

  // 将老式的getUserMedia API封装成Promise
  return function(constraints) {
    return new Promise((resolve, reject) => {
      getUserMedia.call(navigator, constraints, resolve, reject);
    });
  }
}

// 使用示例
const getMedia = getUserMediaSupport();
getMedia({
  audio: {
    sampleRate: 44100,      // 采样率
    channelCount: 1,        // 声道数
    echoCancellation: true  // 回声消除
  },
  video: false
})
.then(stream => {
  // 成功获取音频流
  console.log('获取音频流成功:', stream);
})
.catch(err => {
  // 处理错误
  console.error('获取音频流失败:', err);
});

二、权限问题

在获取权限的时候,需要考虑到权限问题,比如麦克风权限,需要用户授权,否则无法获取音频流。 而限制上由于http协议是明文传输,所以无法保证音频流的安全性,所以浏览器的安全策略会限制获取音频流。

注意了,当获取不到权限的时候,考虑一下你的域名是否是http的协议,如果是http的协议不行的,需要使用https的协议或者localhost本地才能获取音频流

三、音频流的格式问题

音频流格式涉及编码和容器格式两个层面。以下是常见的Web音频格式:

1. 容器格式:

  • WebM: 由Google开发的开源多媒体容器格式,基于Matroska
  • Ogg: 由Xiph.Org基金会开发的开源多媒体容器格式
  • MP4: 基于ISO/IEC 14496-14标准的多媒体容器格式
  • MPEG: Moving Picture Experts Group制定的标准容器格式

2. 编解码器:

Opus是一个完全开放、免版税且多功能的音频编解码器,由IETF于2012年标准化。它具有以下特点:

  • 支持从6 kbit/s窄带语音到510 kbit/s全带立体声音乐的比特率
  • 算法延迟从2.5ms到60ms可调
  • 支持恒定比特率(CBR)和可变比特率(VBR)编码
  • 支持语音和音乐编码
  • 支持多种采样率(8-48 kHz)
  • 内置丢包隐藏和前向纠错功能

在Web录音中常用的MIME类型:

  • audio/webm: WebM容器,默认使用Opus编解码器
  • audio/webm;codecs=opus: 显式指定使用Opus编解码器的WebM容器
  • audio/ogg;codecs=opus: Ogg容器使用Opus编解码器
  • audio/mp4: MP4容器格式
  • audio/mpeg: MPEG容器格式

WebM+Opus组合在Web应用中被广泛采用,原因是:

  1. 开源免费,无专利费用
  2. 编码效率高,音质好
  3. 浏览器支持广泛
  4. 延迟低,适合实时通信
  5. 具有先进的音频处理能力(回声消除、降噪等)

webm 可以转成mp3格式,但是转成mp3格式后,不支持回声消除和降噪。

录音代码

那我们写一个封装好的类,来获取音频流,并保存为mp3格式。

标识挺清晰的,可以参考一下。

简单的录音类

class AudioRecorder{
  constructor(){
    this.mediaRecorder = null   // 媒体实例
    this.stream = null          // stream 音频流
    this.audioChunks = []       // 音频的数据
    this.recordStartTime = null // 录音了开始时间
    this.isRecording = false    // 是否开启录音
  }

  //初始化录音
  async init(){
    try {
      if(this.stream){
        this.stream.getTracks().forEach(track => track.stop());
      }
      this.mediaRecorder = null

      this.stream = await navigator.mediaDevices.getUserMedia({
        audio: {
          sampleRate: 44100,        // 降低采样率到标准CD音质
          channelCount: 1,          // 使用单声道
          echoCancellation: true,   // 开启回声消除
          noiseSuppression: true,   // 开启降噪
          autoGainControl: true     // 开启自动增益
        } 
      })

      // 'audio/mpeg',
      // 'audio/webm',
      // 'audio/webm;codecs=opus',
      // 'audio/ogg;codecs=opus',
      // 'audio/mp4'  

      // 使用标准音频
      const mimeType = 'audio/webm;codecs=opus'  // WebM 格式通常支持较好

      this.mediaRecorder = new MediaRecorder(this.stream, {
        mimeType: mimeType,
        audioBitsPerSecond: 12800,  // 更高的比特率
      })

      // 设置ondataavailable事件处理
      this.mediaRecorder.ondataavailable = (event) => {
        if(event.data.size > 0){
          this.audioChunks.push(event.data)
        }
      }

      return true
    } catch (error) {
      console.error('录音初始化失败:', error)
      throw error // 抛出问题
    }
  }

  // 开始录音
  start(){
    if(!this.mediaRecorder || this.mediaRecorder.state !== 'inactive') return false
    try {
      this.audioChunks = [] 
      this.stream = null
      this.recordStartTime = Date.now()
      this.mediaRecorder.start(100) // 每100ms触发一次 ondataavailable,可以获取这100ms内的录音数据
      this.isRecording = true
      return true
    } catch (error) {
      console.error('开始录音失败:', error) 
      this.isRecording = false
      return false
    }

  }

  // 停止录音
  stop(){
    if (!this.mediaRecorder || this.mediaRecorder.state !== 'recording') return null

    return new Promise((resolve) =>{

      const startTime = this.recordStartTime

      // 设置 onstop 事件处理器
      this.mediaRecorder.onstop = () =>{
        const duration = Date.now() - startTime
        const audioBlob = new Blob(this.audioChunks, {type: this.mediaRecorder.mimeType})
        const audioFile = new File([audioBlob], `voice_${Date.now()}.mp3`, {
          type: this.mediaRecorder.mimeType
        })



        resolve({
          file: audioFile,
          duration: duration
        })
      }

      this.mediaRecorder.stop()
      this.isRecording = false

      if(this.stream){
        this.stream.getTracks().forEach(track => track.stop())
      }

      this.recordStartTime = null

    })

  }


  // 销毁实例
  destroy() {
    if (this.stream) {
      this.stream.getTracks().forEach(track => track.stop())
    }
    this.mediaRecorder = null
    this.audioChunks = []
  }

}

export default AudioRecorder

上面的代码,并不固定,你可以根据你的需求,来修改代码。

使用方法

常用的方法:

  1. 初始化录音
const audioRecorder = new AudioRecorder()
audioRecorder.init()
  1. 开始录音
audioRecorder.start()
  1. 停止录音
audioRecorder.stop()
  1. 销毁实例
audioRecorder.destroy()

业务逻辑参考

一些业务逻辑,需要自己去实现,比如:

  1. 录音的时长限制:小于2000ms,则不保存,并提示用户说话时间太短等等。
  2. 录音的格式转换:mp3格式。
  3. 录音的保存:保存到本地或者上传到服务器。
  4. 录音的播放:播放录音,可以存变量之后本地拉取播放、也可以去服务器请求到内容再去播放。