项目中音频播放用原生组件 audio
就可以了,界面也挺美观,功能也很齐全,但是界面样式每个浏览器不一样,所以设计师都会设计一套独立的音频播放组件。
先来看看浏览器原生 audio
组件长什么样,一定要加 controls
,显示原生控制按钮。
再来看看自定义音频组件长什么样,是可以正常播放的。
00:00/
00:00
# 实现思路
想要实现一个自定义音频播放组件,还得使用原生 audio
组件,只不过不加 controls
属性,播放控制按钮以及进度条手动实现,UI实现起来比较简单,难点也只有进度条的拖动,要用到三个鼠标事件,在控制进度的小圆点上监听 mousedown
事件,mousemove
事件监听鼠标移动,实时计算鼠标偏移量并设置小圆点的 left
属性,设置播放进度条的 width
属性,达到进度条跟随鼠标移动的效果,mouseup
监听鼠标松开事件,更新音频播放进度。
要特别注意的是 mousedown
事件可以绑定在小圆点上,mousemove
不能绑定在小圆点上,必须绑定在 document
上,如果绑定在小圆点上会出现鼠标拖动太快,小圆点位置没有及时更新,下次 mousemove
就监听不到了,所以需要在 mousedown
时绑定 mousemove
和 mouseup
事件到 document
上,然后在 mouseup
的时候解绑。
thumbMouseDown(e) {
e.stopImmediatePropagation()
e.preventDefault()
document.addEventListener('mousemove', this.thumbMouseMove, false)
document.addEventListener('mouseup', this.thumbMouseUp, false)
},
thumbMouseMove(e) {
console.log(e)
}
thumbMouseUp() {
document.removeEventListener('mousemove', this.thumbMouseMove, false)
document.removeEventListener('mouseup', this.thumbMouseUp, false)
},
音频文件下载一般都需要个过程,如果还没准备好是不能播放的,canplay 事件在音频可以开始播放时触发。完整的事件参考手册 (opens new window)
# 音频流
如果后端返回的不是一个可以播放的 url,而是通过流推送过来,则需要特殊处理下,需要用到 createObjectURL (opens new window) 创建一个 url。后端返回的 content-type 要设置为 audio/wav
fetch('/url')
.then(res => {
return res.blob()
})
.then(res => {
const blob = new Blob([res])
const url = URL.createObjectURL(blob)
// 这里的 url 可以直接赋值给 audio src 播放
})
完整代码
<template>
<div class="audio-play">
<audio
ref="audio"
preload
:src="src"
@canplay="canplay"
@pause="pause"
@timeupdate="timeupdate"
>
您的浏览器不支持音频
</audio>
<div class="audio-controls">
<div class="audio-play-icons" @click="play">
<i v-if="playing" class="iconfont iconzanting1 color-primary"></i>
<i
v-else
class="iconfont iconbofang1"
:class="[audio ? 'color-primary' : 'is-disabled']"
></i>
</div>
<div ref="progress" class="audio-progress">
<div class="audio-progress__background" @click.self="clickProgress"></div>
<div class="audio-progress__track" :style="trackStyle" @click.self="clickProgress"></div>
<div
class="audio-progress__thumb"
ref="thumb"
:style="thumbStyle"
:class="{ 'is-disabled': !audio }"
@mousedown="thumbMouseDown"
></div>
</div>
<div class="flex-center">
<p class="font-small color-text-3">{{ currentTime }}/</p>
<p class="font-small color-text-3">{{ totalTime }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
src: String
},
data() {
return {
duration: 0, // 总时长
currentTime: '00:00',
totalTime: '00:00',
playing: false,
audio: null,
progress: 0,
initClientX: 0,
initOffsetX: 0
}
},
computed: {
trackStyle() {
return {
width: `${this.progress}%`
}
},
thumbStyle() {
return {
left: `${this.progress}%`
}
}
},
methods: {
// 播放
play() {
if (this.audio) {
// 播放当前录音前先停止当前页面所有 audio 播放
if (!this.playing) {
const audios = document.getElementsByTagName('audio')
audios.forEach(audio => {
audio.pause()
})
}
this.playing ? this.audio.pause() : this.audio.play()
this.playing = !this.playing
}
},
pause() {
this.playing = false
},
// 切换进度
clickProgress(e) {
const progress = e.offsetX / this.$refs.progress.clientWidth
this.audio.currentTime = this.duration * progress
},
canplay() {
this.audio = this.$refs.audio
if (this.audio) {
this.duration = this.audio.duration
this.totalTime = this.formatDuration(Math.ceil(this.duration))
}
},
// 进度条更新
timeupdate() {
if (this.audio) {
this.currentTime = this.formatDuration(Math.ceil(this.audio.currentTime))
this.progress = (this.audio.currentTime / this.duration) * 100
if (this.audio.currentTime === this.audio.duration) {
this.audio.pause()
this.playing = false
}
}
},
// 小圆点鼠标按下事件
thumbMouseDown(e) {
if (!this.audio) return
this.audio.pause()
this.playing = false
this.initClientX = e.clientX
this.initOffsetX = (this.$refs.progress.clientWidth * this.progress) / 100
e.stopImmediatePropagation()
e.preventDefault()
document.addEventListener('mousemove', this.thumbMouseMove, false)
document.addEventListener('mouseup', this.thumbMouseUp, false)
},
thumbMouseMove(e) {
let progress =
(e.clientX - this.initClientX + this.initOffsetX) / this.$refs.progress.clientWidth
progress = progress <= 1 ? (progress >= 0 ? progress : 0) : 1
this.progress = progress * 100
},
thumbMouseUp() {
this.audio.currentTime = (this.duration * this.progress) / 100
document.removeEventListener('mousemove', this.thumbMouseMove, false)
document.removeEventListener('mouseup', this.thumbMouseUp, false)
},
// 格式化分钟
formatDuration(duration) {
if (duration > 0) {
let minute = Math.floor(duration / 60)
minute = minute < 10 ? '0' + String(minute) : String(minute)
let second = duration % 60
second = second < 10 ? '0' + String(second) : String(second)
return minute + ':' + second
} else {
return '00:00'
}
}
}
}
</script>
<style lang="scss" scoped>
@import url('//at.alicdn.com/t/font_2436495_253nh3fvpe5.css');
.audio-play {
display: flex;
align-items: center;
height: 32px;
background: #F7F7F7;
border-radius: 16px;
padding-right: 16px;
div {
box-sizing: border-box;
}
}
.audio-controls {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.audio-play-icons__text {
font-size: 12px;
line-height: 18px;
color: #FFFFFF;
}
.audio-play-icons {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 12px;
height: 24px;
width: 24px;
i {
font-size: 24px;
&.is-disabled {
color: #999999;
cursor: not-allowed;
}
}
}
.audio-progress {
height: 4px;
position: relative;
width: 100%;
margin: 0 8px;
flex-grow: 1;
cursor: pointer;
}
.audio-progress__background {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: #DDDDDD;
border-radius: 4px;
}
.audio-progress__track {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: #466796;
border-radius: 4px;
}
.audio-progress__thumb {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid #FFFFFF;
background: #466796;
top: -4px;
margin-left: -5px;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
&.is-disabled {
background: #999999;
cursor: not-allowed;
}
}
.font-small {
font-size: 12px;
line-height: 18px;
}
.flex-center {
display: flex;
align-items: center;
}
.color-primary {
color: #466796;
}
</style>