一、前置知识点
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, // 当前音乐时间s
currentValue: '00:00', // 转换成时间展示
duration: 0, // 当前音乐总时间s
durationValue: '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, // 当前音乐时间s
currentValue: '00:00', // 转换成时间展示
duration: 0, // 当前音乐总时间s
durationValue: '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