nuxt中自定义音频播放组件

写在前面

在nuxt中自定义音频播放组件,遇到了一个问题:当存在多个音频播放器时,一次只允许播放一个音频。Google了大半天,写出来的效果并不好,后面老大出手解决了,觉得他的这个方式很精巧,记录下以备忘。

最终效果

做一个最简单的自定义音频播放组件AudioPlayer,props中只有一个参数:文件的source地址。

效果图:

实现细节

为了实现这个效果,需要将功能拆分下:

1、获取音频文件的时长duration,这个可以通过audio的canplay事件。

2、获取音频的currentTime, 这个可以通过audio的timeupdate事件

3、用变量state来显示audio的三种状态:paused, playing, stopped,并分别定义三种状态下的方法: play(), pause(), stop(),根据用户操作来调用不同的方法。

4、定义numberToStardardMinuteSecond函数,将currentTime,duration处理成时间,这里引入库numbro来做这件事。

5、自定义进度条,添加click事件处理函数seek,通过获取点击位置占整个长度的百分比,换算出此时audio的currentTime,设置audio的currentTime

6、使用eventbus进行组件间的通信,在audio play之前,触发beforeplay事件,同时在AuidoPlay组件中监听beforeplay事件。

分步来:

先布局出音频播放组件的模子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div class="d-flex w-100 align-items-top">
<div class="audio-player mr-2">
<audio :src="src" ref="audio" />
<i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
<i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
</div>
<div class="w-100">
<span class="float-right text-muted small">{{ currentTime }} / {{ mediaDuration }}</span>
<span class="small">点击后开始播放</span>
<div class="progress">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>
</div>
</div>
</template>

<script>
import numbro from 'numbro'

export default {
props: [ 'src' ],
data() {
return {
currentTime: 0,
mediaDuration: 0,
state: 'stopped'
}
},
computed: {
currentPercentage() {
if (this.mediaDuration === 0) {
return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
} else {
return numbro(this.currentTime / this.mediaDuration ).format({ output: 'percent', mantissa: 0 })
}
}
}
}
</script>

Step1: 获取duration

在audio上添加事件监听@canplay,在methods中定义对应的处理函数onCanplay。

1
2
3
4
5
6
7
8
<audio :src="src" ref="audio" @canplay="onCanplay" />

.....

onCanplay(event) {
const audio = event.target
this.mediaDuration = parseInt(audio.duration)
}

Step2: 获取currentTime

在audio上添加事件监听@timeupdate,在methods中定义对应的处理函数onTimeupdate. 这里调用的stop函数,会在下一步定义。

1
2
3
4
5
6
7
8
9
10
11
<audio :src="src" @canplay="onCanplay" @timeupdate="onTimeupdate" ref="audio" />
.......
onTimeupdate(event) {
const audio = event.target
if (audio.currentTime >= this.mediaDuration) {
this.currentTime = this.mediaDuration
this.stop()
} else {
this.currentTime = audio.currentTime
}
}

Step3: 在methods中,定义play, pause, stop函数,用以处理audio的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
audio.play()
}
this.state = 'playing'
},
pause() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'paused'
},

stop() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'stopped'
}

Step4: 定义numberToStardardMinuteSecond函数,将currentTime,duration转化为时间。

引入numbro.

1
yarn add numbro

在methods中定义numberToStardardMinuteSecond:

1
2
3
4
5
6
7
8
9
10
numberToStardardMinuteSecond(num) {
const number = parseInt(num)
const time = numbro(number).format({ output: 'time' })

if (time.substr(0, 1) === '0') {
return time.substr(2, time.length - 2)
} else {
return time
}
}

Step5: 在进度条上监听click事件,在methods中定义seek函数来处理click事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="progress" @click="seek">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>

......

seek(event) {
const $this = $(event.target)

const widthclicked = event.pageX - $this.offset().left
const totalWidth = $this.width()

this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
}

为了使progress可以点击,需要在style中给progress设置pointer-events。

1
2
3
.progress {
pointer-events: auto;
}

Step6: 使用eventbus进行组件间的通信,实现一次只播放一个视频。

在audio play之前,$emit一个事件beforeplay, 同时将audio作为参数传出去。

修改play函数,添加事件派发:

1
2
3
4
5
6
7
8
9
10
11
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
this.$nuxt.$emit('beforeplay', audio)
audio.play()
}
this.state = 'playing'
}

任何其他的组件都可以监听这个beforeplay事件,当然也包括AudioPlayer组件。

在AudioPlayer组件添加对beforeplay的监听:

1
2
3
4
5
6
7
8
9
10
created() {
this.$nuxt.$on('beforeplay', (player) => {
if (player === this.$refs.audio) {
return
}
if (this.state === 'playing') {
this.pause()
}
})
}

这里判断传过来的参数,如果是自身emit了这个事件,就跳过,如果是其他audio,同时自身还在播放,则自身停止播放。

OK,大功告成。

最终的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div class="d-flex w-100 align-items-top">
<div class="audio-player mr-2">
<audio :src="src" @timeupdate="onTimeupdate" @canplay="onCanplay" ref="audio" />
<i v-if="state === 'playing'" class="far fa-pause-circle fa-2x" @click="pause"></i>
<i v-if="state != 'playing'" class="far fa-play-circle fa-2x" @click="play"></i>
</div>
<div class="w-100">
<span class="float-right text-muted small">{{ numberToStardardMinuteSecond(currentTime) }} / {{ numberToStardardMinuteSecond(mediaDuration) }}</span>
<span class="small">点击后开始播放</span>
<div class="progress" @click="seek">
<div class="progress-bar bg-primary" style="transition-duration: 0.2s; transition-timing-function: linear;" role="progressbar" :style="`width: ${currentPercentage}`"></div>
</div>
</div>
</div>
</template>

<script>
import $ from 'jquery'
import numbro from 'numbro'

export default {
props: [ 'src' ],
data() {
return {
currentTime: 0,
mediaDuration: 0,
state: 'stopped'
}
},
created() {
this.$nuxt.$on('beforeplay', (player) => {
if (player === this.$refs.audio) {
return
}
if (this.state === 'playing') {
this.pause()
}
})
},
computed: {
currentPercentage() {
if (this.mediaDuration === 0) {
return numbro(this.mediaDuration).format({ output: 'percent', mantissa: 0 })
} else {
const progress = this.currentTime / this.mediaDuration
return numbro(progress).format({ output: 'percent', mantissa: 0 })
}
}
},
methods: {
play() {
const audio = this.$refs.audio
if (this.state === 'stopped') {
audio.currentTime = 0
}
if (audio.paused) {
this.$nuxt.$emit('beforeplay', audio)
audio.play()
}
this.state = 'playing'
},

pause() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'paused'
},

stop() {
const audio = this.$refs.audio
if (!audio.paused) {
audio.pause()
}
this.state = 'stopped'
},

onTimeupdate(event) {
const audio = event.target
if (audio.currentTime >= this.mediaDuration) {
this.currentTime = this.mediaDuration
this.stop()
} else {
this.currentTime = audio.currentTime
}
},

seek(event) {
const $this = $(event.target)

const widthclicked = event.pageX - $this.offset().left
const totalWidth = $this.width()

this.$refs.audio.currentTime = widthclicked / totalWidth * this.mediaDuration
},

onCanplay(event) {
const audio = event.target
this.mediaDuration = parseInt(audio.duration)
},

numberToStardardMinuteSecond(num) {
const number = parseInt(num)
const time = numbro(number).format({ output: 'time' })

if (time.substr(0, 1) === '0') {
return time.substr(2, time.length - 2)
} else {
return time
}
}
}
}
</script>

参考

Media events

EventBus for Nuxt.js components