记录h5录音功能以及小程序播放音频实现

317 阅读3分钟

前言

最近小程序项目中需要开发录音功能,但是因为需要实现多个小程序共用一个功能,遂定使用web-view内嵌h5的方式,达到“一劳永逸”,那么真的“一劳永逸”了嘛,在这里埋个伏笔。

好的,事实上,并没有。

那么,遇到了哪些问题呢,我们来简单剖析一下。

  1. 用户点击某个元素,达到临界条件需要触发自动播放。
  2. 音频切换倍速导致ios无法触发onend事件
  3. 部分机型(vivo、iphone14 pro)在web-view播放的声音从听筒位置发出,即音量小,其实是因为录音的问题,录音属性配置问题echoCancellation设置为true

自动播放解决:

// 在获取到音频资源后操作dom调用load方法 audioPlayer = this.$refs.audioPlayer
  audioPlayer.load()  

2、3问题解决方案:最终还是将播放页写在了原生小程序页

h5录音功能实现

在录音测试过程中出现了很多问题,以下进行详细说明:
    1. 在http环境即本地环境、或域名协议为http的环境下,由于浏览器安全协议的限制,需要对浏览器进行配置才能正常开始录音。
    解决方法:
        浏览器访问: chrome://flags/#unsafely-treat-insecure-origin-as-secure,将该选项设置为Enabled
    2. 在小程序web-view中调试录音需要将代码提交到测试环境后才能进行录音,切记在https环境下测试录音。
    

录音demo链接

代码如下:

import { Toast, Dialog } from 'vant'

import Recorder from 'recorder-core'
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'

export default class Recorder {
  recorderManager = null
  recoderAuthStatus = false // 是否授权

  duration = 600000 // 录音时长单位ms 10分钟

  stopFn = null // 结束回调
  changRecordStatusFn = null // 状态改变回调
  recordStatus = 0 // 0未开始 1录音中 2结束 3已上传
  onProcessCallback = null // 录音过程中回调
  
  constructor(data) {
    if (data && data.duration) {
      this.duration = data.duration
    }
    if (data && data.stopFn) {
      this.stopFn = data.stopFn
    }
    if (data && data.changRecordStatusFn) {
      this.changRecordStatusFn = data.changRecordStatusFn
    }
    this.recorderManager = Recorder({
      type: 'mp3',
      sampleRate: 16000,
      bitRate: 16,
      audioTrackSet: {
        echoCancellation: true, // 解决录音声音小的终极大招 (巨好用)
        autoGainControl: true,  // 增益开关 (好用)
      },
      onProcess: (buffers, powerLevel, duration, sampleRate) => {
        if (this.onProcessCallback) this.onProcessCallback(duration)
        if (duration > this.duration && this.recordStatus == 1) {
          Dialog.alert({
            title: '提示',
            message: '已停止录音,录音时长不能超过10分钟!',
            confirmButtonColor: '#2F74ED',
            confirmButtonText: '确定',
          })
          this.stopRecorder()
        }
      },
    })
    this.getSetting().then(() => {
        this.startRecorder()
    })
  }
  // 开始录音
  startRecorder() {
    if (!this.recorderManager || !Recorder.IsOpen()) {
      Toast.fail('未打开录音')
      return
    }
    Toast.success('开始录音')
    this.recorderManager.start()
    this.changRecordStatusFn(1)
    this.recordStatus = 1
  }
  // 结束录音
  stopRecorder() {
    if (!(this.recorderManager && Recorder.IsOpen())) {
      Toast.fail('未打开录音')
      return
    }
    this.recorderManager.stop(
      (blob, duration) => {
        this.changRecordStatusFn(2)
        this.recordStatus = 2
        if (duration < 1) {
          this.duration = 1 //不计,未开始
          Toast.fail('录音失败,录音时间太短')
          return
        }
        const file = new File([blob], 'filename.mp3', { type: 'audio/mp3' })
        file.duration = duration
        this.uploadFile(file, duration)
      },
      (s) => {
        Toast.fail('录音失败:' + s)
      }
    )
  }

  // 上传音频文件
  uploadFile(file, duration) {
    const loading = Toast.loading({
      message: '上传中...',
      forbidClick: true,
      loadingType: 'spinner',
    })
    uploadAudio({ file, duration })
      .then((res) => {
        this.stopFn(res.datas.path)
        this.changRecordStatusFn(3)
        this.recordStatus = 3
      })
      .finally(() => loading.clear())
  }

  // 获取麦克风授权状态
  getSetting() {
    return new Promise((resolve, reject) => {
      if (this.recoderAuthStatus) {
        resolve(true)
      }
      this.recorderManager.open(
        () => {
          resolve(true)
        },
        (msg, isUserNotAllow) => {
          this.recoderAuthStatus = false
          console.log(isUserNotAllow + ':' + msg)
          Toast.fail(msg)
          reject(msg)
        }
      )
    })
  }

  // 销毁引用数据
  destory() {
    if (this.recorderManager) {
      this.recorderManager.close()
    }
  }
}

小程序录音功能实现

代码如下:

export default class Voice {
  recorderManager = null;
  recoderAuthStatus = 0; // 是否授权
  playStatus = 0; // 音频播放状态

  duration = 60000; // 录音时长单位ms 1分钟

  constructor(data) {
    const { duration = 60000} = data;
    this.duration = duration;
    
    this.getSetting().then(() => {
      this.initRecord();
      this.startRecorder();
    }).catch(() => {
      this.openSetting();
    });
  }
  // 初始化音频管理器
  initRecord() {
    // 录音
    this.recorderManager = wx.getRecorderManager();
    this.recorderManager.onError(() => {
      console.log('录音器加载错误');
    });

    this.recorderManager.onStop(res => {
      const duration = res.duration; //获取录音时长
      if (duration > this.duration) {
        console.log('已停止录音,录音时间不能超过60秒');
      } else if (duration < 1) {
        this.duration = 1; //不计,未开始
        console.log('录音失败,录音时间太短');
        return;
      }
      this.duration = duration;
      //  this.audioValue = res.tempFilePath;(这里是录音本地文件,如果需要在其他地方播放,则需要上传转换成网络链接)
      console.log(res, 'file:', res.tempFilePath);
      this.uploadFile(res.tempFilePath);
    });
  }
  // 开始录音
  startRecorder() {
    this.recorderManager.start({
      format: 'mp3',
      duration: this.duration
    });
    console.log('开始录音');
  }
  // 结束录音
  stopRecorder() {
    this.recorderManager.stop();
    console.log('停止录音');
  }

  // 上传音频文件
  uploadFile(path) {
    wx.uploadFile({
      url: "api",
      method: 'POST',
      filePath: path,
      name: 'file',
      success(res) {
        const data = JSON.parse(res.data);
        console.log('upload:',res, data);
      },
      fail(res) {
        console.log('媒体文件上传失败');
        return;
      }
    });
  }

  // 获取麦克风授权状态
  getSetting() {
    let that = this;
    return new Promise((resolve, reject) => {
      if (that.recoderAuthStatus) resolve();
      wx.getSetting({
        success(res) {
          if (!res.authSetting['scope.record']) {
            wx.authorize({
              scope: 'scope.record',
              success() {
                that.recoderAuthStatus = true;
                resolve();
              },
              fail() {
                that.recoderAuthStatus = false;
                reject();
              }
            });
          } else {
            that.recoderAuthStatus = true;
            resolve();
          }
        }
      });
    });
  }
  // 打开麦克风授权
  openSetting() {
    let that = this;
    wx.openSetting({
      success(res) {
        if (res.authSetting['scope.record']) {
          that.recoderAuthStatus = true;
          that.init();
        }
      }
    });
  }
}

小程序播放音频

initAudio() {
    const innerAudioContext = wx.createInnerAudioContext({
      useWebAudioImplement: true // 是否使用 WebAudio 作为底层音频驱动,默认关闭。对于短音频、播放频繁的音频建议开启此选项,开启后将获得更优的性能表现。由于开启此选项后也会带来一定的内存增长,因此对于长音频建议关闭此选项
    });
    //监听音频自然播放至结束的事件
    innerAudioContext.volume = 1;
    innerAudioContext.onPlay((res) => { 
      console.log('开始播放');
    });
    innerAudioContext.onEnded(() => {
      //音频自然播放至结束的事件的回调函数
      this.data.audioInfo.status = 0;
    });
    innerAudioContext.onError(res => {
      console.log('error', res);
    });
  },
  //播放录音
  playOrPause() {
    if (!this.data.audioInfo.url) {
      return;
    }
    if (this.data.audioInfo.status != 1) {
      this.data.audioInfo.status = 1;
      this.data.innerAudioContext.src = this.data.audioInfo.url;
      this.data.innerAudioContext.play();
    } else {
      //正在播放状态,则点击暂停
      this.data.audioInfo.status = 2;
      this.data.innerAudioContext.stop();
    }
  },

app.js

onShow() {
    wx.setInnerAudioOption({
      obeyMuteSwitch: false,
      mixWithOther: false,
      speakerOn: true,
      success() {
        console.log('set audio option success');
      },
      fail(e) {
        console.error(e);
      }
    });
  },