草庐IT

录音、上传、播放音频微信小程序实践

校园苦行生 2023-04-04 原文

文章目录

录音、上传、播放音频微信小程序实践

最近上线了一款智能外呼机器人产品,需要开发一款录音、上传、播放音频功能的
微信小程序给录音师配置外呼话术真人录音。

代码已开源,数据均已本地化处理。适合新手参考学习的完整原生微信小程序小项目。

实践分析

依赖接口

主要使用以下 api

  1. wx.getRecorderManager :获取全局唯一的录音管理器 RecorderManager
  2. wx.createInnerAudioContext : 创建内部 audio 上下文 InnerAudioContext 对象

PS.

  1. 默认 audio 组件样式不符合需求,目前只需播放进度条,InnerAudioContext 配合 process 组件实现
  2. InnerAudioContext 退出小程序自动停止播放,需要退出小程序依然可播放请使用背景音频 BackgroundAudioManager 代替

为什么要声明全局变量:

  1. 录音本身就是唯一全局
  2. 语音播放,如果每次离开、进入页面动态生成、销毁(好像有 bug),会有多条音频同时播放,为避免这个问题,使用全局唯一对象管理
const recorderManager: WechatMiniprogram.RecorderManager = wx.getRecorderManager();
const innerAudioContext: WechatMiniprogram.InnerAudioContext = wx.createInnerAudioContext();

录音

  • 录音开始配置
const recordOptions = {
  duration: 10 * 60 * 1000, // 最多录音时长 10 分钟
  sampleRate: 8000, // 采样率
  numberOfChannels: 1, // 1 个录音通道即可
  format: 'wav', // 服务端指定格式
};
recorderManager.start(recordOptions)
  • 初始状态
  • 录音检测是否收到声音,本想利用 RecorderManager.onFrameRecorded 来感知是否收到声音,
    展示波形图,但该事件不支持 wav 格式文件。目前监听到开始事件即显示录音计时。
// 监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。
recorderManager.onFrameRecorded(({ frameBuffer, isLastFrame }) => {
  console.log('frameBuffer.byteLength: ', frameBuffer.byteLength)
  console.log('isLastFrame: ', isLastFrame);
});
  • 监听录音开始事件,设置录音进行中状态,并展示录音计时器
recorderManager.onStart(() => {
  console.log('recorder start');
  this.startClock();
  this.setData({
    ...recordingData,
  });
});
  • 停止录音事件,可以接收到本地录音文件地址、录音时长信息。一般上传文件至 CDN ,然后把地址存储到业务服务器,接着试听播放。
recorderManager.stop();
// 停止录音事件
recorderManager.onStop(async (res) => {
  console.log('recorder stop', res)
  // 停止后立即更新状态,以免异常
  this.stopClock();
  this.setData({
    ...initRData,
  });
  if (isError) {
    isError = false;
    return;
  }
  const { tempFilePath, duration } = res;
  console.log('tempFilePath', tempFilePath);
  const url = await uploadFile({ filePath: tempFilePath });

  // 快速开始时,获取的都是未录音,会冲掉当前上传试听,这里手动设置一下
  if (innerAudioContext.currentTime) {
    innerAudioContext.stop();
  }
  innerAudioContext.src = url;
  this.setData({
    ...initPlayData,
    ...initRData,
    detail: {
      ...this.data.detail,
      url,
      duration: Math.ceil(duration / 1000),
    },
    duration: formatClock(duration, true),
  });
  // await this.getDetail('CUR');
});
  • 监听录音异常、中断,录音异常千奇百怪,且无文档具体说明。
    比如电话会打断录音,触发暂停事件。拒绝授权会出发错误事件。这里都设置异常变量为 true,在 onStop 事件中不进行上传逻辑,而是恢复到录音初始状态。
// 监听录音错误事件
recorderManager.onError((err) => {
  this.noEffectStopRecorder();

  showErrMsg(msgMap[err.errMsg] || err.errMsg || '小程序错误');
  console.log('recorderManager.onError', err);
});

// 监听录音暂停事件
recorderManager.onPause(() => {
  console.log('recorder pause');
  // 立马停止,重新开始,没有恢复机制
  this.noEffectStopRecorder();
});
  • 记录异常不进行业务处理并调用终止录音。这里注意录音不像播放调用 stop 是无副作用的。未开始或暂停录音调用 stop 会抛出异常。小心导致死循环。
noEffectStopRecorder() {
  if (this.data.isRecording) {
    isError = true;
    recorderManager.stop();
  }
}

上传

  • 需小程序后台配置相关业务域名
export function uploadFile({ fileName, filePath }: {
  fileName?: string;
  filePath: string;
}) {
  return new Promise<string>((resolve) => {
    wx.showLoading({
      title: '上传中...',
    });
    const name = fileName || filePath;
    // 获取 CDN token
    getNosToken({ fileName: name }).then((data) => {
      console.log('uploadToken: ', data);
      wx.uploadFile({
        url: 'https://nos.com/',
        name: 'file', // 服务器获取流的参数名
        filePath,
        formData: {
          Object: data.objectName,
          'x-nos-token': data.token,
        },
        success(res) {
          console.log('上传成功回调', res);
          wx.hideLoading();
          const url = `https://cdn.com/${data.objectName}`
          console.log(url);
          resolve(url);
        },
        fail(err) {
          wx.hideLoading();
          wx.showToast({
            title: err.errMsg,
            icon: 'none',
          });
          reject(err);
        },
      })
    });
  });
}

播放

  • 未播放状态
  • 监听播放开始事件,设置播放状态,且展示播放进度条
// 监听音频播放事件
innerAudioContext.onPlay(() => {
  console.log('开始播放');
  this.setData({
    ...playingData,
  });
});
  • 监听音频播放进度更新事件,更新 process 百分比
// 监听音频播放进度更新事件
innerAudioContext.onTimeUpdate(() => {
  console.log('监听音频播放进度更新事件');

  let playPercent = 0;
  const duration = this.data.detail.duration || innerAudioContext.duration;
  try {
    playPercent = Math.ceil(((innerAudioContext.currentTime * 1000) / (duration * 1000)) * 100) || 0;
  } catch (e) {
    playPercent = 0;
  }
  playPercent = playPercent && playPercent > 100 ? 100 : playPercent;
  const currentTime = formatClock(innerAudioContext.currentTime * 1000, true);
  console.log('当前播放时间:', currentTime);
  console.log('微信暴露时间:', innerAudioContext.duration);
  console.log('后端返回时间:', duration);
  console.log('当前播放进度:', playPercent);
  this.setData({
    currentTime,
    playPercent,
  });
});
  • 需求不需要暂停或拖拽进度条。监听音频正常、异常停止或暂停时,都恢复到初始状态。需要恢复或拖拽进度能力,可自行相应事件中处理
// 监听音频自然播放至结束的事件
innerAudioContext.onEnded(() => {
  console.log('监听音频自然播放至结束的事件');
  this.setData({
    ...initPlayData
  });
});

// 监听音频播放错误事件
innerAudioContext.onError((res) => {
  /**
   * 10001	系统错误
   * 10002	网络错误
   * 10003	文件错误
   * 10004	格式错误
   * -1	    未知错误
   */
  console.log(res.errCode, res.errMsg);
  this.setData({
    ...initPlayData
  });
});

// 监听音频暂停事件
innerAudioContext.onPause(() => {
  console.log('监听音频暂停事件');
  this.setData({
    ...initPlayData
  });
});

// 监听音频停止事件
innerAudioContext.onStop(() => {
  console.log('监听音频停止事件');
  this.setData({
    ...initPlayData,
  });
});

Page 事件

  1. 页面初次渲染完成,初始化音频录音、播放事件
  2. 页面每次重新进入加载最新业务数据
  3. 页面离开当前页面或退出小程序,停止录音、播放
/**
   * 生命周期函数--监听页面初次渲染完成
   */
onReady() {
  this.initRecorder();
  this.initAudioPlayer();
},

/**
 * 生命周期函数--监听页面显示
 */
onShow() {
  this.getDetail('CUR');
},

/**
 * 生命周期函数--监听页面卸载
 */
onUnload() {
  console.log('切换页面停止录音或播放');
  innerAudioContext.stop();

  this.noEffectStopRecorder();
},

参考

  1. 原文地址
  2. Github 地址

有关录音、上传、播放音频微信小程序实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  3. ruby-on-rails - 添加回形针新样式不影响旧上传的图像 - 2

    我有带有Logo图像的公司模型has_attached_file:logo我用他们的Logo创建了许多公司。现在,我需要添加新样式has_attached_file:logo,:styles=>{:small=>"30x15>",:medium=>"155x85>"}我是否应该重新上传所有旧数据以重新生成新样式?我不这么认为……或者有什么rake任务可以重新生成样式吗? 最佳答案 参见Thumbnail-Generation.如果rake任务不适合你,你应该能够在控制台中使用一个片段来调用重新处理!关于相关公司

  4. ruby-on-rails - 有没有办法为 CarrierWave/Fog 设置上传进度指示器? - 2

    我在Rails应用程序中使用CarrierWave/Fog将视频上传到AmazonS3。有没有办法判断上传的进度,让我可以显示上传进度如何? 最佳答案 CarrierWave和Fog本身没有这种功能;你需要一个前端uploader来显示进度。当我不得不解决这个问题时,我使用了jQueryfileupload因为我的堆栈中已经有jQuery。甚至还有apostonCarrierWaveintegration因此您只需按照那里的说明操作即可获得适用于您的应用的进度条。 关于ruby-on-r

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  7. 微信小程序通过字典表匹配对应数据 - 2

    前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立

  8. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  9. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  10. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

随机推荐