微信小程序音频功能开发实(cai)践(keng)

2023-10-13 01:48

本文主要是介绍微信小程序音频功能开发实(cai)践(keng),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 需求分析与开发方案

1.1 需求简介

最近产品给我们提出了“在小程序中播放音频课程”的需求,主要是有四个要点:

  • 课程管理:进入某个课程的播放页面,获取全部音频列表,但暂时不播放。
  • 音频管理:支持在播放页面,点击任意音频进行播放;可自动播放下一首。比如这样
  • 进度控件:支持拖动修改进度/上下首/暂停/播放,就像下面这样。
  • 全局播放:当用户暂时离开小程序时,在微信聊天列表页顶部展示背景音频。

就像这样子。

1.2 开发分析

好了,问题来了,怎么实现上面这几个需求呢?

我陷入了沉思…………

第一条“课程管理”不难,全局维护一个数组就好了。

第二条“音频管理”看上去是个麻烦,一开始我想到了小程序提供的audio控件。

但是随即我就否决掉了这种想法,理由主要有两点:

  • 微信官方提供的audio控件有默认的样式,如下图,这与设计稿的需求不相符。
  • 经过在微信官方提供的小程序实例Demo中亲测,如果使用audio控件,那么当我退出当前页面的时候,音频会消失,这没有办法满足PM要求的“全局播放”

因此,我决定采用微信提供的backgroundAudioManager。

1.2.1 backgroundAudioManager简介

按官方文档的说法,backgroundAudioManager是:全局唯一的背景音频管理器

下面列出它的部分重要属性和重要的方法:

属性:

  • duration:当前音频长度,可以用来初始化播放控件的值。
  • currentTime:当前播放的位置,可以用来更新播放控件的进度值
  • paused:false为播放,true表示停止/暂停
  • src:音频数据源,注意设置src的时候会自动播放
  • title:音频标题(刚刚在微信聊天列表页顶部展示的音频title“为什么秋冬季节孩子易生病”,就是通过这里设置的)

方法:

  • play/pause/stop/seek:可以进行音频常见的播放控制,其中seek是跳转到特定播放进度的方法
  • onPlay/onPause/onStop/onEnded:响应特定事件,其中onStop是主动停止,onEnded是自动播放完毕(这可用于实现“连续播放”)
  • onTimeUpdate:背景音频播放进度更新事件,可与前面的currentTime属性结合在一起,去更新控件的值。
  • onWaiting/onCanplay:音频通常不会立刻就能播放,这两个方法可以在音频加载的时候为用户做一些提示。

更多的消息请查看它的官方文档。

1.2.2 播放控件

第三条“播放控件”也不算太难,播放/暂停/上下首都用小图片就可以了。

但是难点在于播放进度条的模拟,前面已经说到audio控件的样式是不符合需求的。

那么我决定采用slider来模拟,应该也可以搞定。

第四条,前面已经说了,用backgroundAudioManager实现“全局播放”。

1.2.3 开发方案确定

好了,需求分析得差不多了,我们要开发这个需求,需要三个对象,

  • 课程管理对象,负责维护课程信息和课程音频列表,不负责播放
  • 音频管理对象,即backgroundAudioManager,负责管理音频的播放,其中只有changeAudio方法具有修改音频的权限
  • 播放控件。

有了这几个对象,课程管理/音频管理/进度控件/全局播放就都可以搞定啦。

不过,话虽然这么说,但是实际实现需求总是会碰到各种各样的问题。

2. 功能实现

因为需求实在太多了,我没法一一列出,在这里就介绍一些需要技巧的需求

2.1 Slider控件模拟进度

前面提到,控件大概长这样


所以得用slider来模拟,但是模拟并不容易。哈?你说为什么?我慢慢告诉你。

2.1.1 需求一:控件随着音频播放,自动更新

PM的需求是:控件随着音频播放,自动更新进度,左值随着进度更新,右值为音频总长度。

但是小程序自带的slider不支持展示左右值,我们只能自己模拟。

<!--  音频进度控件 -->
<view class="course-control-process">// 左值展示,currentProcess<text class="current-process">{{currentProcess}}</text>// 进度条<sliderbindchange="hanleSliderChange"               // 响应拖动事件bindtouchstart="handleSliderMoveStart"bindtouchend="handleSliderMoveEnd"min="0"max="{{sliderMax}}"activeColor="#8f7df0"value="{{sliderValue}}"/>// 右值展示,totalProcess<text class="total-process">{{totalProcess}}</text>
</view> 
currentProcess为左值、totalProcess为右值、sliderMax控件最大值、sliderValue为当前控件的value。

那么,怎么更新这些数值呢?前面提到backgroundAudioManager有一个onTimeUpdate方法,在这里面去更新进度值就可以了。

//  formatAudioProcess函数我就不放了,就是把时间格式化成00:15这样就行了onTimeUpdate() {// 省略一些判断代码self.page.setData({currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),sliderValue: Math.floor(globalBgAudioManager.currentTime)});
},  

这里有一件值得注意的是,就是在进入同一个课程的播放页时,由于原page很可能已经销毁(比如你执行navigateTo),因此需要在初始化的时候更新原有的data值,比如当前的播放进度currentProcess,这就要从当前的backgroundAudioManager里去拿。

## 检查是否同一个课程,如果是的话,更新进度 if (id !== globalCourseAudioListManager.getCurrentCourseInfo().id)## 更新方法 updateControlsInOldAudio() {// 获取当前音频const currentAudio = globalCourseAudioListManager.getCurrentAudio();// 更新进度和控件内容this.setData({currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),sliderValue: formatAudioProcess(globalBgAudioManager.currentTime),sliderMax: Math.floor(currentAudio.duration / 1000) - 1 || 0,totalProcess: formatAudioProcess(currentAudio.duration / 1000 || 0),hasNextAudio: !globalCourseAudioListManager.isRightEdge() && this.data.hasBuy,hasPrevAudio: !globalCourseAudioListManager.isLeftEdge() && this.data.hasBuy,paused: globalBgAudioManager.paused,currentPlayingAudioId: currentAudio.audio_id,courseChapterTitle: currentAudio.title});},  

2.1.2 需求二:拖动进度条,自动跳转到特定位置

注意到前面slider控件具有bindchange="hanleSliderChange",那么我们就可以拿到value值,然后去更新音频了

hanleSliderChange(e) {const position = e.detail.value;this.seekCurrentAudio(position);},// 拖动进度条控件seekCurrentAudio(position) {// 更新进度条const page = this;// 音频控制跳转// 这里有一个诡异bug:seek在暂停状态下无法改变currentTime,需要先play后pauseconst pauseStatusWhenSlide = globalBgAudioManager.paused;if (pauseStatusWhenSlide) {globalBgAudioManager.play();}globalBgAudioManager.seek({position: Math.floor(position),success: () => {page.setData({currentProcess: formatAudioProcess(position),sliderValue: Math.floor(position)});if (pauseStatusWhenSlide) {globalBgAudioManager.pause();}console.log(`The process of the audio is now in ${globalBgAudioManager.currentTime}s`);}});},  

看上去有一点比较奇怪是不是?backgroundAudioManager的seek方法是没有success回调的,这里被我改了。

seek(options) {wx.seekBackgroundAudio(options);  // 这样实现,就可以配置success回调了
}  

但是,“onTimeUpdate事件触发slider控件更新”和“手动拖动触发slider更新”是有冲突的,假如说两个函数都要改slider,听谁的?

但是,可以利用监测touchstart和touchend事件,来检查是否在滑动。如果在滑动,禁止onTimeUpdate去修改slider控件更新就行了。

因此,我先设定一个变量,来标记是否正在滑动

handleSliderMoveStart() {this.setData({isMovingSlider: true});},handleSliderMoveEnd() {this.setData({isMovingSlider: false});}, 
在滑动期间禁止更新进度条即可
onTimeUpdate() {// 在move的时候,不要更新进度条控件if (!self.page.data.isMovingSlider) {self.page.setData({currentProcess: formatAudioProcess(globalBgAudioManager.currentTime),sliderValue: Math.floor(globalBgAudioManager.currentTime)});}// 其他省略
}, 
2.2 backgroundAudioManager相关需求

在开始下一个需求介绍之前,不知道各位有没有疑问:

我在哪儿设置的onTimeupdate方法?

OK,我来介绍下。

首先,全局获取

this.backgroundAudioManager = wx.getBackgroundAudioManager(); 

其次,在play/index.js中引入backgroundAudioManager

let globalBgAudioManager = app.backgroundAudioManager; 

在适当的时候,比如我就是onLoad,扩展globalBgAudioManager对象。——这样我就把具体的功能放进了具体的page中,不同的page中针对backgroundAudioManager可以有不同的实现。

this.initBgAudioListManager();  

接下来我们看看这个拓展到底干了什么。

initBgAudioListManager() {// options中的函数在执行的时候,this指向函数本身(亲测),因此这里需要保存Page对应的this。const page = this;const self = globalBgAudioManager;const options = {// options在后面会介绍};// decorateBgAudioListManager函数,直接修改globalBgAudioManager对象,从而实现方法的拓展globalBgAudioManager = decorateBgAudioListManager(globalBgAudioManager, options);  

好了,怎么引入的现在已经说完了,接下来就讲需求,也就是介绍options里面干了什么。

其实options里面都是backgroundAudioManager已经有的方法,具体可以参考文档。我只是做了改写

2.2.1 需求三:绕过onCanPlay,提醒用户音频在加载

众所周知,音频需要加载一段时间才可以播放,为此小程序的全局播放对象,即backgroundAudioManager提供了onWaiting和onCanplay,看上去天生就是为了音频加载的交互实现的。

但不知道为什么,onCanplay无!法!触!发!和社区提了这个问题也没有人鸟我哎……心痛。

算了算了,他强由他强,我绕我的墙。。。

首先,在options中,改写onWaiting:先提示用户正在加载当中,isWaiting进行标记(“看!音频在Waiting!”)

const options = {onWaiting() {wx.showLoading({title: '音频加载中…'});globalBgAudioManager.isWaiting = true;},
} 

然后接下来,在时间进度发生更新的时候(这相当于开始播放了),把Loading窗口关了就行。同样是在options中去改写onTimeUpdate。

onTimeUpdate() {if (self.isWaiting) {self.isWaiting = false;setTimeout(() => {wx.hideLoading();}, 300);// 设置300ms是为了避免某些音频加载过快而导致Loading效果一闪而过对用户造成糟糕的体验}// 以下代码省略
},  

2.2.2 需求四:点击某个音频,实现播放

这个需求的麻烦之处,在于需要检查点击的音频是什么,比如假定你在播放音频A,你重新点击A,那当然不用重播了啊。

以及iOS版本的小程序和阿里云服务器似乎有点过节,下面就会看到。

在pages/play/index内部,先响应点击事件

## pages/play/indexoutlineOperation(e) {// 获取音频地址const courseAudio = e.currentTarget.dataset.outline || {};const targetAudioId = courseAudio.audio_id;// 中间省略一系列合法性检查。this.playTargetAudio(targetAudioId);},  

然后执行播放相关操作,这个globalCourseAudioListManager虽然前面提到过,但是一会儿再具体介绍,它做了什么就直接看注释好了

## pages/play/index/*** 点击/自动播放 目标音频* @param {*Number} targetAudioId* - 检查是否点击到同一个音频* - 检查是否完全播放完毕* - 若未播放完毕,或者点击的不是同一个音频,先暂停当前音频* - 执行音频播放操作*/playTargetAudio(targetAudioId) {const currentAudio = globalCourseAudioListManager.getCurrentAudio();// 点击未停止的原音频的话,没必要响应if (targetAudioId === currentAudio.audio_id && !!globalBgAudioManager.currentTime) {return false;} else {this.getAudioSrc(targetAudioId).then(() => {// 若未暂停,则先暂停if (!globalBgAudioManager.paused) {globalBgAudioManager.pause();}// 全局切换当前播放的音频index(此时还没有开始播放)globalCourseAudioListManager.changeCurrentAudioById(targetAudioId);// 更新当前控件状态,比如新音频的title和长度,总要更新吧。this.updateControlsInNewAudio();// 更换并且播放背景音乐globalBgAudioManager.changeAudio();});}}, 

好了,终于到这个changeAudio函数了,它也是刚刚提到的options里面的一部分。

## changeAudio是options的属性,被扩展进入了backgroundAudioManager// 修改当前音频changeAudio() {// 获取并且const { url, audio_id, title, content_type_signare_url } = globalCourseAudioListManager.getCurrentAudio();const { doctor, name, image } = globalCourseAudioListManager.courseInfo;self.title = title;self.epname = name;self.audioId = audio_id;self.coverImgUrl = image;self.singer = doctor.nickname || '丁香医生';// iOS使用content_type_signare_urlconst src = isIOS() ? content_type_signare_url : url;if (!src) {showToast({title: '音频丢失,无法播放',icon: 'warn',duration: 2000});} else {self.src = src;}
}  

为什么这里iOS要用content_type_signare_url?(它是我们后端返回的一个字段)

因为iOS小程序发起音频文件请求的时候,会默认带上content-type:octet-stream,而我们的音频文件URL又带有Signatrue签名参数,阿里云服务器似乎会默认把content-type加入到签名当中……于是我就遇上了403错误。

解决方案有两个:

  • 让后端负责CDN服务器的同事,在我请求获取音频src地址之前,先请求一次资源,并且做好缓存。
  • 把音频地址改成公开的。

2.3 courseAudioListManager相关需求

前面提到,我需要维护一个全局的课程信息和音频列表的管理对象,然后,就能操作音频列表了。

## 在app.js当中初始化
this.courseAudioListManager = createCourseAudioListManager();## 在pages/play/index.js里面引用
const globalCourseAudioListManager = app.courseAudioListManager; 

这个对象其实没有太多好介绍的,比较简单。又比如,前面提到“点击某个音频并自动播放”,其中有一步是这样的。

// 全局切换当前播放的音频index(此时还没有开始播放)
globalCourseAudioListManager.changeCurrentAudioById(targetAudioId);  

就是根据id来修改音频的索引,它是这么干的。

changeCurrentAudioById(audioId = -1) {this.currentIndex = this.audioList.findIndex(audio => audio.audio_id === audioId);
},  

其他,具体有哪些方法,可以看前面的1.2.3节“开发方案确定”中的脑图。

不过,它有个addAudioSrc,可以解决重播失败的问题。

2.3.1 用重新加载src的方法,解决重播失败

当一个音频的播放被“停止”而不是“暂停”的时候,再调用play()方法,是不会重播的,亲测调用seek方法执行跳转也不行。

比如,当我试听完了一段音频,想重新听的时候,常规的play是无能的……怎么办?当然是绕过去啊

当你点击播放按钮的时候,

  • 首先通过一系列检查,就会触发下面这个playTargetAudio
handleStartPlayClick() {// 以上省略,若globalBgAudioManager.currentTime为false,表示认为你在点击一个已经播放完毕的音频} else if (!globalBgAudioManager.currentTime) {this.playTargetAudio(currentAudio.audio_id);} else // 以下省略
}  
  • 在playTargetAudio内部依次执行getAudioSrc/changeCurrentAudioById/changeAudio
this.getAudioSrc(targetAudioId).then(() => {// 省略// 全局切换当前播放的音频indexglobalCourseAudioListManager.changeCurrentAudioById(targetAudioId);// 省略// 更换并且播放背景音乐globalBgAudioManager.changeAudio();});} 
  • 在getAudioSrc内部,主要的作用就是,更新了一下新的src

globalCourseAudioListManager.addAudioSrc(res.items[0]);

然后我们看看addAudioSrc干了什么

## 现在在courseAudioListManager内部addAudioSrc(audioSrcObject) {this.audioList = this.audioList.map(audio => {// 强制更新特定id的audio对象// 新的src隐藏在audioSrcObject里面if (Number(audio.audio_id) === Number(audioSrcObject.id)) {return Object.assign(audio, audioSrcObject, { id: audio.id });} else {return audio;}});},  

现在src已经更新完了。看上去每次获取到的音频src都指向同一个音频,但是,音频的src地址是带有时间戳的,这避免了缓存,backgroundAudioManager设置src的时候,就会重新加载了~

当然这样,就没有缓存了,交互上会有所牺牲,每次重播的时候都会闪一下“音频加载中”。

如果各位有好的办法实现缓存,欢迎交流哈。

3. 其他一些经验

  • 如果代码过长,不要用三目运算符,很难读。
  • 音频播放可能出现错误,需要用onError加以捕获。
  • 最后,欢迎留言~!

本文作者: 丁香园F2
原文地址:微信小程序音频功能开发实(cai)践(keng)-实战教程-小程序社区-微信小程序-微信小程序开发社区-小程序开发论坛-微信小程序联盟

这篇关于微信小程序音频功能开发实(cai)践(keng)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/199941

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta