最近刚学完vuejs,为了增进理解就写了个MP3播放器(用到了vuex,vue-router,es6)。可上传歌曲歌词封面文件,可管理歌曲,可点击封面放大图片等
项目地址:https://github.com/variinlkt/MP3-vueplayer
项目截图:
项目还有很多不完善的地方,欢迎交流
在这里总结一下在练手时遇到的知识点:
html5的audio对象属性、方法、事件
图片旋转动画
黑胶背景
点击放大
歌词滚动逻辑
播放逻辑
切换歌曲逻辑
一些细枝末节
部署上线
了解html5中的audio对象是写好一个音频播放器的关键
audioTracks 返回表示可用音频轨道的 AudioTrackList 对象
autoplay 设置或返回是否在加载完成后随即播放音频
controller 返回表示音频当前媒体控制器的MediaController 对象
controls 设置或返回音频是否显示控件(比如播放/暂停等)
crossOrigin 设置或返回音频的 CORS 设置
currentSrc 回当前音频的 URL
defaultMuted 设置或返回音频默认是否静音
defaultPlaybackRate 设置或返回音频的默认播放速度
duration 返回当前音频的长度(以秒计)
error 返回表示音频错误状态的 MediaError 对象
loop 设置或返回音频是否应在结束时重新播放
mediaGroup 设置或返回音频所属的组合(用于连接多个音频元素)
networkState 返回音频的当前网络状态
paused 设置或返回音频是否暂停
played 返回表示音频已播放部分的 TimeRanges 对象
preload 设置或返回音频是否应该在页面加载后进行加载
readyState 返回音频当前的就绪状态
seeking 返回用户是否正在音频中进行查找
src 设置或返回音频元素的当前来源
textTracks 返回表示可用文本轨道的 TextTrackList 对象
volume 设置或返回音频的音量
该项目用到的属性:
volume
,crossOrigin
,duration
,src
,currentTime
,ended
,loop
其中设置crossOrigin
属性为“anonymous”,不设置就无法通过js来动态更改audio对象的src
addTextTrack() 在音频中添加一个新的文本轨道
canPlayType() 检查浏览器是否可以播放指定的音频类型
fastSeek() 在音频播放器中指定播放时间。
getStartDate() 返回一个新的Date对象,表示当前时间轴偏移量
load() 重新加载音频元素
play() 开始播放音频
pause() 暂停当前播放的音频
这里用到load
,play
,pause
当视频/音频(audio/video)已经加载后,视频/音频(audio/video)的时长从 “NaN” 修改为正在的时长。
在视频/音频(audio/video)加载过程中,事件的触发顺序如下:
onloadstart在浏览器开始寻找指定视频/音频(audio/video)触发。
ondurationchange在视频/音频(audio/video)的时长发生变化时触发。
onloadedmetadata在指定视频/音频(audio/video)的元数据加载后触发。
onloadeddata在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发。
onprogress在浏览器下载指定的视频/音频(audio/video)时触发。
oncanplay在用户可以开始播放视频/音频(audio/video)时触发。
oncanplaythrough在视频/音频(audio/video)可以正常播放且无需停顿和缓冲时触发。
这里用到ondurationchange
,用于切换歌曲时获取歌曲时长
这里可以看到:播放音乐时歌曲封面是随着歌曲播放而旋转的,播放暂停则暂停旋转
实现原理:css3的animation
html:
<div class="circle">
<div class="circle-inside" :style="{backgroundImage: 'url('+cov+')',animationPlayState:ps?'running':'paused'}">
div>
div>
//ps为当前的播放状况playstate
css:
.circle-inside{
width: 100px;
height: 100px;
background: #fff;
background-size: cover;
position: relative;
top:25%;
left: 25%;
border: 1px solid transparent;
border-radius: 50%;
transform: translate(-50%,-50%);
transform-origin: 50%,50%;
animation: spin 10s linear infinite;//animation
transition: transform .5s;
transform: scale(1,1);
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}
实现原理:linear-gradient
.circle{
border: 1px solid transparent;
background: linear-gradient(45deg,#1B1B1B 35%,#777676 50%,#1B1B1B 65%);//linear-gradient
width: 200px;
height: 200px;
border-radius: 50%;
}
项目中可以点击封面放大如下图所示:
实现原理:css3的transition&transform
.circle-inside{
width: 100px;
height: 100px;
background: #fff;
background-size: cover;
position: relative;
top:25%;
left: 25%;
border: 1px solid transparent;
border-radius: 50%;
transform: translate(-50%,-50%);//transform
transform-origin: 50%,50%;//缩放中心
animation: spin 10s linear infinite;
transition: transform .5s;
transform: scale(1,1);
}
.circle-inside:active{
transform: scale(2,2);
animation: none;
}
在载入页面时计算当前屏幕高度,算出歌词div的marginTop
在载入页面时获取歌词文件及每句歌词的时间,分别放入lrcData和tarr数组中
获取当前时间ct(currenttime)并侦听,设置idx(当前播放第几句歌词),如果发现当前时间ct比tarr[idx]大,则marginTop-30px,idx++
由以上逻辑我们能写出代码:
//script
mounted(){
this.calMarginT(this)//1
this.postData(localStorage.getItem('lrc'),this)//2
},
watch:{//3
ct(){
this.handleTime(this.tarr,this)
},
}
computed:{
ct(){
return this.$store.state.current
}
}
//main.js
Vue.prototype.postData=(url,that)=>{
let lrc=[],tarr=[]
axios.post('api/getLrc',{
lrc:url
})
.then(function(response){
if(response.data){
let lyric=response.data
let larr=lyric.split('\n')
larr.forEach((val,i)=>{
let t=val.split(']')[0]
let time=t.split('[')[1]
let l=val.split(']')[1]
lrc.push(l)
tarr.push(parseInt(time.split(':')[0])*60+parseFloat(val.split(':')[1]))
that.lrcData=lrc
that.tarr=tarr
})
}
})
}
Vue.prototype.calMarginT=(that)=>{
that.marginT=parseInt(window.innerHeight)/2-100
}
Vue.prototype.handleTime=(tarr,that)=>{
if(that.ct>=tarr[that.idx]){
that.marginT-=30
that.idx++
}
}
但是如果我们拉动了进度条就会出bug
解决办法:
Vue.prototype.sliderChange=(newct,tarr,marginT,idx,that)=>{
let minv=10000,mini
tarr.forEach((val,i)=>{
if(Math.abs(val-newct)
that.marginT+=(idx-mini)*30
that.idx=mini
}//在拉进度条时调用
playMusic(url=localStorage.getItem('url'),that=this){
let audi=document.getElementById('audi')
audi.setAttribute("crossOrigin","anonymous")
audi.setAttribute("src","")
audi.setAttribute("src",url)
audi.load()
audi.onduratiοnchange=()=>{//ondurationchange在事件循环最后执行
that.d=audi.duration
}
if(!that.ps) { //play
that.iconChange='el-icon-error'//更改图标
audi.play()
} else { //pause
that.iconChange='el-icon-caret-right'
audi.pause()
}
if(that.ct!==audi.currentTime){//暂停时更改播放位置,否则相当于停止
audi.currentTime=that.ct
}
that.ps=!that.ps//更改播放状态
},
//store.js
mutations:{//1
update(state,info){//多参数必须是个对象
state.playSong=info.song
state.playSinger=info.singer
state.playCov=info.cov
state.playLrc=info.lrc
state.playUrl=info.url
state.playIdx=info.idx
},
init(state){
state.current=0
state.marginT=0
state.tarr=[]
state.idx=0
state.lrc=[]
state.playState=false
},
},
//App.vue
changeSong(s){
let audi = document.getElementById('audi')
audi.setAttribute("src","") //停止当前歌曲的播放
let i=this.playIdx
if(s==='next'){//点击下一首
i++
if(i===this.tableLength)//如果当前歌曲是最后一首歌
i=0
}
else if(s==='pre'){//点击上一首
i--
if(i<0)
i=this.tableLength-1
}
else i=s
store.commit('update',{
song:this.tableData[i].song,
singer:this.tableData[i].singer,
cov:this.tableData[i].cov,
lrc:this.tableData[i].lrc,
url:this.tableData[i].url,
idx:this.tableData[i].idx
})
this.setStorage(this.song,this.singer,this.cov,this.url,this.lrc)
this.playMusic(this.url)
this.calMarginT(this)
this.postData(localStorage.getItem('lrc'),this)
},
//main.js
Vue.prototype.setStorage=(song,singer,cov,url,lrc)=>{
if(!localStorage.getItem('song')){//播放第一首
localStorage.setItem('song',song)
localStorage.setItem('singer',singer)
localStorage.setItem('cov',cov)
localStorage.setItem('url',url)
localStorage.setItem('lrc',lrc)
}else{
if(localStorage.getItem('url')!==url){//播放下一首
if(url){
localStorage.setItem('song',song)
localStorage.setItem('singer',singer)
localStorage.setItem('cov',cov)
localStorage.setItem('url',url)
localStorage.setItem('lrc',lrc)
}
}
}
}
computed:{//要写在computed中而不是data中
marginT:{
get: function () {//getter
return this.$store.state.marginT
},
set: function (newVal) {//setter
this.$store.state.marginT=newVal
}
}
}
update(state,info){//info必须是个对象
state.playSong=info.song
state.playSinger=info.singer
state.playCov=info.cov
state.playLrc=info.lrc
state.playUrl=info.url
state.playIdx=info.idx
},
:style="{backgroundImage: 'url('+cov+')',animationPlayState:ps?'running':'paused'}"
this.$store.commit()
:import {mapMutations} from 'vuex'
...mapMutations([
'update','init'
]),
待完善中…