前言
本功能是基于 vue3 + ts 实现的。之所以选择网易云作为练手项目,主要是因为他的API是开源的,相对齐全。 网易云API地址:binaryify.github.io/NeteaseClou…
先上效果图
一. 开发步骤
1. 歌单实现逻辑
1.1 首先获取歌单id(这里我拿的是推荐歌单:/personalized)
推荐歌单
说明 : 调用此接口 , 可获取推荐歌单可选参数 : limit: 取出数量 , 默认为 30 (不支持 offset)
接口地址 : /personalized
调用例子 : /personalized?limit=1
接口返回值:(接口返回值太多了,就此省略。大家可以用postman请求查看)
1.2 获取歌单详情
获取歌单详情
说明 : 歌单能看到歌单名字, 但看不到具体歌单内容 , 调用此接口 , 传入歌单 id, 可 以获取对应歌单内的所有的音乐(未登录状态只能获取不完整的歌单,登录后是完整的),但是返回的trackIds是完整的,tracks 则是不完整的,可拿全部 trackIds 请求一次 song/detail 接口获取所有歌曲的详情 (github.com/Binaryify/N…)必选参数 : id : 歌单 id
可选参数 : s : 歌单最近的 s 个收藏者,默认为8
接口地址 : /playlist/detail
调用例子 : /playlist/detail?id=24381616
注意!!!
这个接口返回的仅仅是歌单部分内容,并不包含歌曲url - 播放地址(即mp3)。因此还需要额外请求一个获取音乐url的接口。
如果想要完整的歌单,就先拿接口返回的trackIds再去请求song/detail。(这里就不一一说明了)
1.3 获取音乐url
获取音乐 url
说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url,未登录状态或者非会员返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)必选参数 : id : 音乐 id
可选参数 : br: 码率,默认设置了 999000 即最大码率,如果要 320k 则可设置为 320000,其他类推
接口地址 : /song/url
调用例子 : /song/url?id=33894312 /song/url?id=405998841,33894312
拿到音乐url后就可以调用HTML audio元素实现音频播放啦。(具体往下看)
这里总结下:
先获取 歌单id -> 然后获取 歌曲id -> 最后获取 音频
反过来就是,要拿音频地址就要先获取歌曲id,所以你能拿到歌曲id的话,歌单获取这步就可以省略了。
2. 播放音频逻辑实现
好啦,歌单逻辑说完了。接下来就是讲如何实现播放了。
2.1 使用audio实现
一般来说,直接
<audio src="音频地址" controls />
这样就已经能实现一个音频播放了。但如果要自定义样式的话,就不需要加controls(设置或返回音频是否应该显示控件(比如播放/暂停等))。那么就要自己做上一首播放,下一首播放,暂停播放,开始播放等功能了。
实现一个简单的音频播放器,首先要了解下audio的一些属性和方法。这里我就先列举几个等下要用到的。 具体可以看 www.w3school.com.cn/jsref/dom_o…
Audio 对象属性
属性 | 概述 |
---|---|
paused | 设置或返回音频是否暂停。 |
src | 设置或返回音频的 src 属性的值。 |
duration | 返回音频的长度(以秒计)。 |
controls | 设置或返回音频是否应该显示控件(比如播放/暂停等)。 |
ended | 返回音频的播放是否已结束。 |
Audio 对象方法
方法 | 概述 |
---|---|
play() | 开始播放音频。 |
pause() | 暂停当前播放的音频。 |
canplay() | 当浏览器能够开始播放指定的音频/视频时,会发生 canplay 事件。 |
timeupdate() | timeupdate 事件在音频(audio)的播放位置发生改变时触发。 |
2.2 实现Audio Controls功能
一个简易的音乐播放器,一共分为四大功能:上一首播放、下一首播放、暂停播放、开始播放。
需要你们自己加上控制按钮,然后绑定相关事件。如图:
HTML
<audio
ref="audio"
:src="currentPlayUrl"
@canplay="getDuration"
@pause="pause"
@timeupdate="timeupdate"
@play="play"
/>
开始播放
setup() {
const audio = ref(); // 这里是拿到audio的dom元素
// 开始播放音频
function audioPlay() {
audio.value.play();
};
// dom元素加载完后开始播放音乐
onMounted(() => {
audioPlay();
});
return {
audio
};
}
暂停播放
/**
* 控制播放按钮
* 通过paused属性,判断当前音频播放状态
*/
function controlPlay() {
if(!audio.value.paused) {
audio.value.pause(); // 停止播放
} else {
audio.value.play(); // 开始播放
}
};
改变播放按钮状态
function play() {
// 展示播放按钮
};
function pause() {
// 展示暂停按钮
};
上一首播放、下一首播放
首先要先定义一个歌单列表,然后定义一个当前播放的索引值以及当前播放的url
const state = reactive({
playList: [
{ url: 'http://m7.music.126.net/20210326150405/a91efaab5690d7966eff4f8104ae575e/ymusic/9ba6/4a9a/e903/eaca05cc36e0d64a2cd104722f6f9cc4.mp3' },
{ url: 'http://m7.music.126.net/20210326154148/251d2d32b08419744ca682170d98bf35/ymusic/0409/520c/5158/3c17fbba85efa6b428d2f2dafd5de326.mp3' },
{ url: 'http://m8.music.126.net/20210326162043/b8af89f1ac2579ea17035b51a7656f7f/ymusic/7634/c63c/a0c1/ad330e06c7f3c791d065af5c255e002c.mp3' }
],
currentIndex: 0
});
const currentPlayUrl = computed(() => {
return state.playList[state.currentIndex];
});
/**
* 上一首播放
* 若当前播放索引值 = 0(第一首),则播放歌单最后一首,否则播放上一首
*/
function prevPlay() {
state.currentIndex = state.currentIndex === 0 ? state.playList.length - 1 : state.currentIndex - 1;
// 这里要延迟播放,因为要先让它加载一下
nextTick(() => {
audioPlay();
});
}
/**
* 下一首播放
* 若当前播放索引值 = 歌单列表长度(最后一首),则播放歌单第一首,否则播放下一首
*/
function prevPlay() {
state.currentIndex = state.currentIndex === state.playList.length - 1 ? 0 : state.currentIndex + 1;
nextTick(() => {
audioPlay();
});
}
到这里,功能就讲完啦。剩下的就是歌曲信息的展示啦。关于歌曲信息这块,我就重点说下如何获取音频时长吧。
3. 获取音频时长
注意:获取音频时长主要是靠Audio的duration这个属性获取的。但是如果在音频还未可以播放的时候,是不可以获取到它的时长的。
当音频/视频处于加载过程中时,会依次发生以下事件:
- loadstart
- durationchange
- loadedmetadata
- loadeddata
- progress
- canplay
- canplaythrough
因此要到canplay这一步才能获取到音频时长。
function getDuration() {
// 此时可以拿到音频时长(audio.value.duration);
}
二. 完整代码
注:我这里的数据结构格式都是按照网易云接口的数据格式来的。数据结构格式你们可以自己自定义,重点关注逻辑就好了。
<template>
<div class="audio__wrap">
<audio
ref="audio"
:src="currentPlayUrl"
@timeupdate="timeupdate"
@canplay="getDuration"
@play="play"
@pause="pause">
</audio>
<div
class="audio__progress"
:class="[showCircle ? 'show' : 'notShow']"
@mouseover="showCircle = true"
@mouseleave="showCircle = false">
<el-slider
v-model="currentTime"
:min="0"
:max="endTime"
:show-tooltip="false"
:show-input-controls="false"
@change="changeCurrentTime" />
</div>
<div class="audio__block">
<img
:src="infoList[currentIndex].al.picUrl"
class="img" />
<div class="info__block">
<div>
<span class="name">{{ infoList[currentIndex].name }} </span>
<span class="line">-</span>
<span
v-for="item in infoList[currentIndex].ar"
:key="item.id"
class="singerName">{{ item.name }}
</span>
<div class="time">{{ calTime(currentTime) }} / {{ calTime(endTime) }}</div>
</div>
</div>
<div class="icon-controls">
<i
class="iconfont icon-diyiyeshouyeshangyishou prev"
@click="prevPlay()" />
<play-icon
:width="45"
:height="45"
:iconSize="26"
:isPlay="isPlay"
@click="controlPlay" />
<i
class="iconfont icon-zuihouyiyemoyexiayishou next"
@click="nextPlay()" />
</div>
<div class="list-controls">
<i class="iconfont icon-liebiaoxunhuan list-circul" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, ref, toRefs } from "@vue/runtime-core";
import { nextTick } from 'vue'
import PlayIcon from '@/components/icon/PlayIcon.vue';
import { calTime } from '@/utils/utils';
export default defineComponent({
components: { PlayIcon },
setup() {
const audio = ref();
const state = reactive({
isPlay: false,
currentTime: 0,
endTime: 0,
showCircle: false,
// 当前播放的歌单url
playList: [
{ url: 'http://m7.music.126.net/20210326150405/a91efaab5690d7966eff4f8104ae575e/ymusic/9ba6/4a9a/e903/eaca05cc36e0d64a2cd104722f6f9cc4.mp3' },
{ url: 'http://m7.music.126.net/20210326154148/251d2d32b08419744ca682170d98bf35/ymusic/0409/520c/5158/3c17fbba85efa6b428d2f2dafd5de326.mp3' },
{ url: 'http://m8.music.126.net/20210326162043/b8af89f1ac2579ea17035b51a7656f7f/ymusic/7634/c63c/a0c1/ad330e06c7f3c791d065af5c255e002c.mp3' }
],
// 歌曲信息
infoList: [
{
id: 1393394219,
name: 'Toothbrush',
al: {
id: 81949037,
picUrl: 'https://p1.music.126.net/dGrgYPQEde-xk1oSAZjyyA==/109951164820048861.jpg',
},
ar: [
{ id: 28867131, name: 'KikoBlob' }
]
},
{
id: 25962464,
name: 'Tom And Jerry',
al: {
id: 2348299,
picUrl: 'https://p1.music.126.net/7-v1gIVbimGDkQ9ALE6zvg==/6042915906752898.jpg',
},
ar: [
{ id: 0, name: 'Bradley' }
]
},
{
id: 5041188,
name: 'Elements',
al: {
id: 500661,
picUrl: 'https://p1.music.126.net/Eu7h6JAEftfSweGLNeDzBA==/6649846325500946.jpg',
},
ar: [
{ id: 80585, name: 'Tom Lehrer' }
]
},
],
currentIndex: 0
});
// 当前播放的url
const currentPlayUrl = computed(() => {
return state.playList[state.currentIndex].url;
});
/**
* 播放音乐
*/
function audioPlay() {
audio.value.play();
state.isPlay = true;
};
/**
* 更新当前时间
* 如果当前音频进度 = 总时长,则自动播放下一首
*/
function timeupdate(e: any) {
state.currentTime = e.target.currentTime;
if(e.target.currentTime === state.endTime) {
nextPlay();
}
};
/**
* 获取音乐时长
*/
function getDuration() {
state.endTime = audio.value.duration;
};
/**
* 通过进度条改变当前音频进度
* @param value 当前的值
*/
function changeCurrentTime(value: number) {
audio.value.currentTime = value;
state.currentTime = value;
};
/**
* 上一首播放
*/
function prevPlay() {
state.currentIndex = state.currentIndex === 0 ? state.playList.length - 1 : state.currentIndex - 1;
nextTick(() => {
audioPlay();
});
};
/**
* 下一首播放
*/
function nextPlay() {
state.currentIndex = state.currentIndex === state.playList.length - 1 ? 0 : state.currentIndex + 1;
nextTick(() => {
audioPlay();
});
};
/**
* 控制播放按钮
*/
function controlPlay() {
state.isPlay = !state.isPlay;
if(!audio.value.paused) {
audio.value.pause();
} else {
audio.value.play();
}
};
function play() {
state.isPlay = true;
};
function pause() {
state.isPlay = false;
};
onMounted(() => {
audioPlay();
});
return {
...toRefs(state),
audio,
timeupdate,
getDuration,
currentPlayUrl,
prevPlay,
nextPlay,
controlPlay,
changeCurrentTime,
calTime,
play,
pause,
};
}
});
</script>
<style lang="scss">
.audio__wrap {
position: fixed;
bottom: 0;
min-width: 1200px;
width: 100%;
@include background_color('background-global');
}
.audio__progress {
.el-slider__runway {
width: 100%;
height: 5px;
margin-bottom: 15px;
border-radius: 3px;
position: relative;
cursor: pointer;
vertical-align: middle;
@include background_color('background-dot');
}
.el-slider__bar {
height: 6px;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
position: absolute;
background: #d33a31;
}
.el-slider__button {
display: inline-block;
width: 20px;
height: 20px;
vertical-align: middle;
border: 2px solid #d33a31;
background-color: #d33a31;
border-radius: 50%;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: .2s;
transition: .2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
.audio__block {
display: flex;
align-items: center;
width: 100%;
margin: 0 20px 15px 20px;
@include background_color('background-global');
.img {
width: 70px;
height: 70px;
border-radius: 8px;
}
.info__block {
margin-left: 20px;
height: 70px;
line-height: 35px;
.name {
@include font_color('text-primary');
&:hover {
cursor: pointer
}
}
.line {
margin-left: 10px;
@include font_color('text-second');
}
.singerName {
@include font_color('text-second');
font-size: 14px;
margin-left: 10px;
&:hover {
cursor: pointer;
@include font_color('text-second-actived');
}
}
.time {
@include font_color('text-gray');
font-size: 14px;
}
}
.icon-controls {
display: flex;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
.prev, .next {
color: #d33a31;
font-size: 40px !important;
margin: 0 20px;
cursor: pointer;
}
}
.list-controls {
position: absolute;
right: 150px;
.list-circul {
@include font_color('text-primary');
font-size: 24px;
}
}
}
.show .el-slider__button-wrapper {
display: block;
}
.notShow .el-slider__button-wrapper {
display: none;
}
</style>
这是最近在做的一个项目,当时做这个也遇到了不少坑。
现在记下坑,希望对你们有帮助。
或许你们有更好的方法,欢迎大家分享嘻嘻~