一、前置知识点
1.1 音频组件控制-uni.createInnerAudioContext()
创建并返回内部 audio 上下文 innerAudioContext
对象。
主要用于当前音乐播放;
1.1.1 innerAudioContext属性
属性 | 类型 | 说明 | 只读 | 平台差异说明 |
---|---|---|---|---|
src | String | 音频的数据链接,用于直接播放。 | 否 | 微信小程序不支持本地路径 |
startTime | Number | 开始播放的位置(单位:s),默认 0 | 否 | |
autoplay | Boolean | 是否自动开始播放,默认 false | 否 | H5端部分浏览器不支持 |
loop | Boolean | 是否循环播放,默认 false | 否 | |
obeyMuteSwitch | Boolean | 是否遵循系统静音开关,当此参数为 false 时,即使用户打开了静音开关,也能继续发出声音,默认值 true | 否 | 微信小程序、百度小程序、抖音小程序、飞书小程序、京东小程序、快手小程序(仅在 iOS 上生效) |
duration | Number | 当前音频的长度(单位:s),只有在当前有合法的 src 时返回,需要在onCanplay中获取 | 是 | |
currentTime | Number | 当前音频的播放位置(单位:s),只有在当前有合法的 src 时返回,时间不取整,保留小数点后 6 位 | 是 | |
paused | Boolean | 当前是是否暂停或停止状态,true 表示暂停或停止,false 表示正在播放 | 是 | |
buffered | Number | 音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲。 | 是 | |
volume | Number | 音量。范围 0~1。 | 否 | |
sessionCategory | String | 设置音频播放模式,可取值:“ambient” - 不中止其他声音播放,不能后台播放,静音后无声音; “soloAmbient” - 中止其他声音播放,不能后台播放,静音后无声音; “playback” - 中止其他声音,可以后台播放,静音后有声音。 默认值为"playback"。 | 否 | App 3.3.7+ |
playbackRate | Number | 播放的倍率。可取值:0.5/0.8/1.0/1.25/1.5/2.0,默认值为1.0 | 否 | App 3.4.5+(Android 需要 6 及以上版本)、微信小程序 2.11.0、支付宝小程序、抖音小程序 2.33.0+、快手小程序、百度小程序 3.120.2+ |
1.1.2 innerAudioContext方法列表
方法 | 参数 | 说明 |
---|---|---|
play | 播放(H5端部分浏览器需在用户交互时进行) | |
pause | 暂停 | |
stop | 停止 | |
seek | position | 跳转到指定位置,单位 s |
destroy | 销毁当前实例 | |
onCanplay | callback | 音频进入可以播放状态,但不保证后面可以流畅播放 |
onPlay | callback | 音频播放事件 |
onPause | callback | 音频暂停事件 |
onStop | callback | 音频停止事件 |
onEnded | callback | 音频自然播放结束事件 |
onTimeUpdate | callback | 音频播放进度更新事件 |
onError | callback | 音频播放错误事件 |
onWaiting | callback | 音频加载中事件,当音频因为数据不足,需要停下来加载时会触发 |
onSeeking | callback | 音频进行 seek 操作事件 |
onSeeked | callback | 音频完成 seek 操作事件 |
offCanplay | callback | 取消监听 onCanplay 事件 |
offPlay | callback | 取消监听 onPlay 事件 |
offPause | callback | 取消监听 onPause 事件 |
offStop | callback | 取消监听 onStop 事件 |
offEnded | callback | 取消监听 onEnded 事件 |
offTimeUpdate | callback | 取消监听 onTimeUpdate 事件 |
offError | callback | 取消监听 onError 事件 |
offWaiting | callback | 取消监听 onWaiting 事件 |
offSeeking | callback | 取消监听 onSeeking 事件 |
offSeeked | callback | 取消监听 onSeeked 事件 |
1.1.3 简单示例
// 创建innerAudioContext对象
const innerAudioContext = uni.createInnerAudioContext();
// 开始自动播放
innerAudioContext.autoplay = true;
// 设置音频地址
innerAudioContext.src = 'url.mp3';
// 开始播放的回调函数
innerAudioContext.onPlay(() => {console.log('开始播放');
});
// 播放报错的事件监听
innerAudioContext.onError((res) => {console.log(res.errMsg);console.log(res.errCode);
});
1.2 背景音频控制-uni.getBackgroundAudioManager()
获取全局唯一的背景音频管理器 backgroundAudioManager
。
主要是实现后台播放功能,退出app或者小程序后还能继续播放,同时状态栏有控制播放状态控件;
1.2.1 backgroundAudioManager属性列表
属性 | 类型 | 说明 | 只读 |
---|---|---|---|
duration | Number | 当前音频的长度(单位:s),只有在当前有合法的 src 时返回 | 是 |
currentTime | Number | 当前音频的播放位置(单位:s),只有在当前有合法的 src 时返回 | 是 |
paused | Boolean | 当前是是否暂停或停止状态,true 表示暂停或停止,false 表示正在播放 | 是 |
src | String | 音频的数据源,默认为空字符串,**当设置了新的 src 时,会自动开始播放,**目前支持的格式有 m4a, aac, mp3, wav | 否 |
startTime | Number | 音频开始播放的位置(单位:s) | 否 |
buffered | Number | 音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲。 | 是 |
title | String | 音频标题,用于做原生音频播放器音频标题。原生音频播放器中的分享功能,分享出去的卡片标题,也将使用该值。 | 否 |
epname | String | 专辑名,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 | 否 |
singer | String | 歌手名,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 | 否 |
coverImgUrl | String | 封面图url,用于做原生音频播放器背景图。原生音频播放器中的分享功能,分享出去的卡片配图及背景也将使用该图。 | 否 |
webUrl | String | 页面链接,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 | 否 |
protocol | String | 音频协议。默认值为 ‘http’,设置 ‘hls’ 可以支持播放 HLS 协议的直播音频,App平台暂不支持 | 否 |
playbackRate | Number | 播放的倍率。可取值:0.5/0.8/1.0/1.25/1.5/2.0,默认值为1.0。(App 3.4.5+、微信基础库 2.11.0+、支付宝小程序、抖音小程序 2.33.0+、快手小程序、百度小程序 3.120.2+) | 否 |
1.2.2 backgroundAudioManager方法列表
方法 | 参数 | 说明 |
---|---|---|
play | 播放 | |
pause | 暂停 | |
stop | 停止 | |
seek | position | 跳转到指定位置,单位 s |
onCanplay | callback | 背景音频进入可以播放状态,但不保证后面可以流畅播放 |
onPlay | callback | 背景音频播放事件 |
onPause | callback | 背景音频暂停事件 |
onStop | callback | 背景音频停止事件 |
onEnded | callback | 背景音频自然播放结束事件 |
onTimeUpdate | callback | 背景音频播放进度更新事件 |
onPrev | callback | 用户在系统音乐播放面板点击上一曲事件(iOS only) |
onNext | callback | 用户在系统音乐播放面板点击下一曲事件(iOS only) |
onError | callback | 背景音频播放错误事件 |
onWaiting | callback | 音频加载中事件,当音频因为数据不足,需要停下来加载时会触发 |
1.2.3 简单示例
// 创建bgAudioManager对象
const bgAudioManager = uni.getBackgroundAudioManager();
bgAudioManager.title = '音乐标题';
bgAudioManager.singer = '作者';
bgAudioManager.coverImgUrl = '封面图片';
bgAudioManager.src = 'url.mp3';
// 开始播放的回调函数
bgAudioManager.onPlay(() => {console.log('开始播放');
});
// 播放报错的事件监听
bgAudioManager.onError((res) => {console.log(res.errMsg);console.log(res.errCode);
});
1.2.4 注意点
注意 因为背景音频播放耗费手机电量,所以平台都有管控,需在manifest中填写申请。
- ios App平台,背景播放需在manifest.json -> app-plus -> distribute -> ios 节点添加
"UIBackgroundModes":["audio"]
才能保证音乐可以后台播放(打包成ipa生效) - 小程序平台,需在manifest.json 对应的小程序节点下,填写"requiredBackgroundModes": [“audio”]。发布小程序时平台会审核
二、音乐功能点
2.1 实现效果
- 控制播放暂停;
- 实现上一首,下一首;
- 展示当前音乐当前进度时间,结束时间;展示当前音乐当前进度时间,结束时间;
- 可以通过进度条去控制音乐跳转到对应时间点;
- 实现倍速播放;
- 后台播放;
2.2 获取音乐信息
- 当前音乐播放状态;
- 音乐列表数据,便于实现上一首下一首;
- 当前音乐播放时长,与结束时长,播放速度;
- 音乐的地址、封面图片、名称等基础信息;
- 歌词展示,到达当前歌词时歌词高亮;
三、实现步骤
3.1 技术选型
通过前置知识点我们了解到了uni.createInnerAudioContext()和uni.getBackgroundAudioManager()的实例属性方法等。
可以根据需求去选择性调用实例,前者可以在小程序中调用来播放音乐;而如果想要退出小程序或者app后依然可以让音乐继续播放,这个时候就可以使用后者来生成悬浮框,以及状态栏中显示;
结论:
- 小程序或app中使用uni.createInnerAudioContext();
- 退出小程序或app时使用uni.getBackgroundAudioManager();
3.2 实现思路
根据前面整理的音乐所需功能点,我们需要使用store存储一些全局音乐状态信息;便于切换到其他界面的时候可以同步得到最新的音乐信息。
- 首先在程序初始化时,实例化一个audio对象挂载到vue原型上;
- 用于实时获取当前播放信息;
- 初始化时就可以把需要的监听事件挂载,配合实际业务场景;
3.3 简易代码示例
3.3.1 实例化挂载audio对象
入口文件实例化-main.js
// 新建音乐对象挂载到原型上
const innerAudioContext = uni.createInnerAudioContext();
// 音乐播放报错监听
innerAudioContext.onError((res) => {console.log("音乐播放报错监听", res);
});
// 音乐加载中监听
innerAudioContext.onWaiting((res) => {console.log("音乐加载中监听", res);
});
Vue.prototype.$AudioContext = innerAudioContext;
3.3.2 定义一个简单的音乐列表
{
// 滚动条信息
playInfo: {progressWidth: 2, // 滚动条currentTime: 0, // 当前音乐时间scurrentValue: '00:00', // 转换成时间展示duration: 0, // 当前音乐总时间sdurationValue: '00:00', // 总时间转换展示 },
// 简易歌曲列表
audioList: [ {title: '未完成之前',src: 'https://music.163.com/song/media/outer/url?id=1453946527.mp3',id: 1453946527,}, {title: '鲜花',src: 'https://music.163.com/song/media/outer/url?id=2086327879.mp3',id: 2086327879,},{title: '水星记',src: 'https://music.163.com/song/media/outer/url?id=441491828.mp3',id: 441491828,}, {title: '人生有时候懂得放弃',src: 'https://music.163.com/song/media/outer/url?id=2139388989.mp3',id: 2139388989,},{title: '精卫',src: 'https://music.163.com/song/media/outer/url?id=1951069525.mp3',id: 1951069525,},
],
}
3.3.3 实现暂停播放、切换音乐
要实现播放音乐首先要给$AudioContext设置音乐地址src,这样才能通过地址去获取对应的音乐信息。
首选需要在页面一加载时默认配置列表中第一首歌的地址:
onLoad() {this.$AudioContext.playbackRate = 2;if (this.audioList.length) {this.$AudioContext.src = this.audioList[this.currentIndex].src;this.currentTitle = this.audioList[this.currentIndex].title;this.setPlayInfo();}// 开始播放获取音乐信息this.$AudioContext.onPlay((e) => {this.setPlayInfo();});this.$AudioContext.onEnded((e) => {// 结束播放去播放下一首this.hanleAudioChange();});
},
播放按钮: 判断paused的状态选择性去调用播放和暂停方法;
切换音乐: 点击音乐列表行更新音乐地址src,停止当前音乐,最后调用播放方法即可;
// 音乐播放
handlePlay() {this.$AudioContext.paused ? this.$AudioContext.play() : this.$AudioContext.pause();this.paused = this.$AudioContext.paused;this.recursionDeep();
},
// 选择目标音乐播放
handleCurrentAudio(index) {this.currentIndex = index;this.currentTitle = this.audioList[index].title;// 先停止当前音乐this.$AudioContext.stop();// 更换播放地址this.$AudioContext.src = this.audioList[index].src;// 播放音乐this.handlePlay();
},
3.3.4 进度条功能实现
首先明确功能点:
- 根据音乐播放进度条增加,显示当前播放时长和总时长(可以根据定时任务获取最新音乐播放信息);
- 可以拖动选择特定位置播放(通过touchmove与touchend事件监听实现);
- 点击某一位置直接跳转对应位置播放(通过点击事件获取x轴位置)
// 递归循环获取最新音乐进度信息
recursionDeep() {clearTimeout(this.timer);if (this.paused) {return};this.timer = setTimeout(() => {if (!this.isMove) {this.setPlayInfo();this.recursionDeep();}}, 500)
},
// 进度条点击事件
progressClick(event) {const {x} = event.detail;const progressWidth = Math.floor(x / this.progressParentWidth * 100);this.playInfo.progressWidth = progressWidth > 100 ? 100 : progressWidth;console.log("event", event);this.progressMouseDown();
},
// 音乐进度条移动监听
progressMouseMove(event) {if (!this.$AudioContext.src) {return;}this.isMove = true;const {pageX} = event.changedTouches[0];const progressWidth = Math.floor(pageX / this.progressParentWidth * 100);this.playInfo.progressWidth = progressWidth > 100 ? 100 : progressWidth;
},
// 音乐进度条停止监听
progressMouseDown(event) {this.isMove = false;const currentTime = Math.floor(this.$AudioContext.duration * (this.playInfo.progressWidth / 100));this.$AudioContext.seek(currentTime);this.setPlayInfo();if (!this.$AudioContext.paused) {this.$AudioContext.pause();}this.handlePlay();
},
四、完整源码示例
4.1 示例图
4.2 main.js入口函数
import App from './App'import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'// 新建音乐对象挂载到原型上
const innerAudioContext = uni.createInnerAudioContext();
// 音乐播放报错监听
innerAudioContext.onError((res) => {console.log("音乐播放报错监听", res);
});
// 音乐加载中监听
innerAudioContext.onWaiting((res) => {console.log("音乐加载中监听", res);
});
Vue.prototype.$AudioContext = innerAudioContext;const app = new Vue({...App
})
app.$mount()
4.3 index.vue界面
<template><view class="content"><scroll-view class="main-container" scroll-y><view class="line_box" :class="currentIndex === index ? 'line_box bgc_line' : 'line_box'"v-for="(item, index) in audioList" :key="item.id" @click="handleCurrentAudio(index)">{{item.title}}</view></scroll-view><view class="audio_box"><view class="current_title" v-show="currentTitle">当前播放歌曲:{{currentTitle}}</view><view class="music-progress-bar" @click="progressClick"><view class="progress-bar-line" :style="{width: playInfo.progressWidth + '%'}" @touchmove="progressMouseMove"@touchend="progressMouseDown"></view></view><view class="show_time"><view>{{playInfo.currentValue}}</view><view>{{playInfo.durationValue}}</view></view><view class="audio_control"><view @click="hanleAudioChange('pre')">上一首</view><view @click="handlePlay">{{ paused ? '播放' : '暂停'}}</view><view @click="hanleAudioChange('next')">下一首</view></view></view></view>
</template><script>export default {data() {return {timer: null,currentTitle: '未完成之前',currentIndex: 0,paused: true,isMove: false,playInfo: {progressWidth: 2, // 滚动条currentTime: 0, // 当前音乐时间scurrentValue: '00:00', // 转换成时间展示duration: 0, // 当前音乐总时间sdurationValue: '00:00', // 总时间转换展示 },audioList: [{title: '未完成之前',src: 'https://music.163.com/song/media/outer/url?id=1453946527.mp3',id: 1453946527,},{title: '鲜花',src: 'https://music.163.com/song/media/outer/url?id=2086327879.mp3',id: 2086327879,},{title: '水星记',src: 'https://music.163.com/song/media/outer/url?id=441491828.mp3',id: 441491828,},{title: '人生有时候懂得放弃',src: 'https://music.163.com/song/media/outer/url?id=2139388989.mp3',id: 2139388989,},{title: '精卫',src: 'https://music.163.com/song/media/outer/url?id=1951069525.mp3',id: 1951069525,},],progressParentWidth: 0,}},onLoad() {this.$AudioContext.playbackRate = 2;if (this.audioList.length) {this.$AudioContext.src = this.audioList[this.currentIndex].src;this.currentTitle = this.audioList[this.currentIndex].title;this.setPlayInfo();}this.$AudioContext.onPlay((e) => {// 开始播放获取音乐信息this.setPlayInfo();});this.$AudioContext.onEnded((e) => {// 结束播放去播放下一首this.hanleAudioChange();});},onShow() {this.$nextTick(async () => {const query = uni.createSelectorQuery().in(this);query.select('.music-progress-bar').boundingClientRect(data => {this.progressParentWidth = data ? Math.floor(data.width) : 0;}).exec();});},onUnload() {// 卸载时关闭监听this.$AudioContext.offPlay();this.$AudioContext.offPlay();},methods: {// 进度条点击事件progressClick(event) {const {x} = event.detail;const progressWidth = Math.floor(x / this.progressParentWidth * 100);this.playInfo.progressWidth = progressWidth > 100 ? 100 : progressWidth;console.log("event", event);this.progressMouseDown();},// 音乐进度条移动监听progressMouseMove(event) {if (!this.$AudioContext.src) {return;}this.isMove = true;const {pageX} = event.changedTouches[0];const progressWidth = Math.floor(pageX / this.progressParentWidth * 100);this.playInfo.progressWidth = progressWidth > 100 ? 100 : progressWidth;},// 音乐进度条停止监听progressMouseDown(event) {this.isMove = false;const currentTime = Math.floor(this.$AudioContext.duration * (this.playInfo.progressWidth / 100));this.$AudioContext.seek(currentTime);this.setPlayInfo();if (!this.$AudioContext.paused) {this.$AudioContext.pause();}this.handlePlay();},// 音乐播放handlePlay() {this.$AudioContext.paused ? this.$AudioContext.play() : this.$AudioContext.pause();this.paused = this.$AudioContext.paused;this.recursionDeep();},// 选择目标音乐播放handleCurrentAudio(index) {this.currentIndex = index;this.currentTitle = this.audioList[index].title;// 先停止当前音乐this.$AudioContext.stop();// 更换播放地址this.$AudioContext.src = this.audioList[index].src;// 播放音乐this.handlePlay();},// 递归循环获取最新音乐进度信息recursionDeep() {clearTimeout(this.timer);if (this.paused) {return};this.timer = setTimeout(() => {if (!this.isMove) {this.setPlayInfo();this.recursionDeep();}}, 500)},// 秒转换为分钟secondsToMinutesWithSeconds(seconds) {const minutes = Math.floor(seconds / 60);const remainingSeconds = Math.floor(seconds % 60);return `${this.padZero(minutes)}:${this.padZero(remainingSeconds)}`;},// 补零padZero(number, length = 2) {var str = "" + number;while (str.length < length) {str = "0" + str;}return str;},// 设置播放对象setPlayInfo() {const currentTime = this.$AudioContext.currentTime || 0;const duration = this.$AudioContext.duration || 0;const progressWidth = duration === 0 ? '2' : Math.floor((currentTime / duration) * 100);const currentValue = this.secondsToMinutesWithSeconds(currentTime);const durationValue = this.secondsToMinutesWithSeconds(duration);this.playInfo = {currentTime,duration,progressWidth,currentValue,durationValue};},// 切换歌曲hanleAudioChange(type = 'next') {if (type === 'pre') {this.currentIndex = this.currentIndex === 0 ? this.audioList.length - 1 : this.currentIndex - 1;} else {this.currentIndex = this.currentIndex === this.audioList.length - 1 ? 0 : this.currentIndex + 1;};this.$AudioContext.src = this.audioList[this.currentIndex].src;this.currentTitle = this.audioList[this.currentIndex].title;// 播放歌曲this.handlePlay();},}}
</script><style>.content {width: 100vw;height: calc(100vh - 44px - env(safe-area-inset-top));background-color: #1A262D;color: #fff;}.main-container {width: 100vw;height: 46vh;}.line_box {display: flex;align-items: center;justify-content: center;width: 92%;height: 60px;border-radius: 8px;margin: 10px auto;border: 2px solid #eee;background-color: aquamarine;color: #333;font-weight: bold;}.audio_control {display: flex;align-items: center;justify-content: space-around;color: #333;margin-top: 40px;}.audio_control view {width: 100px;height: 40px;text-align: center;line-height: 40px;background: #edeeab;border: 1px solid #eee;border-radius: 6px;}.audio_box {width: 90%;margin: 30px auto;}.current_title {margin-bottom: 20px;font-weight: bold;font-size: 18px;}.bgc_line {background-color: #e1964b;}.show_time {width: 100%;display: flex;justify-content: space-between;margin-top: 12rpx;}// 音乐进度条.music-progress-bar {position: relative;width: 100%;height: 6rpx;border-radius: 6rpx;background-color: #f3e7d9;.progress-bar-line {position: absolute;top: 0%;left: 0%;width: 2%;height: 100%;border-radius: 6rpx;background-color: #e1964b;}.progress-bar-line::after {content: "";display: inline-block;position: absolute;right: 0%;top: 50%;transform: translateY(-50%);width: 20rpx;height: 20rpx;background-color: #fff;border-radius: 50%;}}
</style>
4.4 项目源码地址
https://gitee.com/yangdacongming/implementation-of-uniapp-music.git