解决Vue项目中audio元素在移动端无法自动播放的问题

问题描述

用 Vue 开发一个 HTML5 活动页面,活动页面需要一个循环播放的背景音乐,一进入页面就自动播放。按照正常思维,直接插入 audio 元素,设置好属性就算完了。就像下面这样:

1
<audio src="path/to/music" loop autoplay></audio>

这样多简单啊,在 PC 浏览器上也表现正常。拿到手机上测试一下,才发现根本不出声音。问题就来了,为啥在手机上不能自动播放呢。

探索过程

使用 google 大法了解到,移动设备为了节省流量,禁用了 audio 元素的自动播放的功能。必须要有用户交互才能播放音频,比如说 touchstart 事件。

了解到这个重要信息,马上也就可以找到相应的解决办法,那就是模拟 touchstart 事件啦,然后把这个事件的触发放到 Vue 的生命周期方法中,比如 mounted

因此马上就做一个音频播放器的组件 AudioPlayer.vue 的组件,具体代码如下:

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
<template>
<div class="player">
<div
class="player-cd"
id="trigger"
>
<audio
:src="src"
id="audio"
loop
></audio>
</div>
</div>
</template>
<script>
export default {
data () {
return {
src: 'http://o9o8lcfa3.bkt.clouddn.com/%E5%88%9A%E5%A5%BD%E9%81%87%E8%A7%81%E4%BD%A0.m4a'
}
},
mounted () {
let trigger = document.getElementById('trigger')
let audio = document.getElementById('audio')
trigger.addEventListener('touchstart', () => {
audio.play()
})

// 模拟触发 「touchstart」 事件
this.createTouchstartEventAndDispatch(trigger)
},
methods: {
createTouchstartEventAndDispatch (el) {
let event = document.createEvent('Events')
event.initEvent('touchstart', true, true)
el.dispatchEvent(event)
}
}
}
</script>
<style lang="less" scoped>
// var
@cdWidth: 50vmin;

// mixins
// 水平垂直居中的 flex 布局
.flex-align-justify-center {
display: flex;
align-items: center;
justify-content: center;
}

// 直径为 width 的圆
.circle (@width) {
width: @width;
height: @width;
border-radius: 50%;
}

// block 伪类
.absolute-block {
content: '';
display: block;
position: absolute;
}

.player {
.flex-align-justify-center;

width: 100vw;
height: 100vh;

&-cd {
.flex-align-justify-center;
.circle(@cdWidth);
background: linear-gradient(#666, #000);

.absolute-circle-block (@color, @width) {
.absolute-block;
.circle(@width);
background: @color;
}

&::before {
.absolute-circle-block(darken(#f00, 5%), 20vmin);
}

&::after {
.absolute-circle-block(#222, 5vmin);
}
}
}
</style>

去浏览器预览的时候发现,上面的代码在 PC 和 Android 手机上和预期一样,可以自动播放。但是在 iOS 上依然不行。通过更进一步的了解,发现在微信内置浏览器内有一种解决办法,利用的是微信内置浏览器的事件 WeixinJSBridgeReady。具体实现见下一小节。

解决方法(不彻底)

目前自动播放功能实现情况如下:

平台 系统自带浏览器 微信内置浏览器
PC ✔️ ✔️
Android ✔️ ✔️
iOS ✔️ ✔️

在实现上述功能的基础上,增加播放旋转,点击切换播放、暂停功能。最终组件代码如下:

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
117
118
119
120
121
122
123
124
125
126
127
128
129
<template>
<div class="player">
<div
class="player-cd"
id="trigger"
:style="{transform: 'rotate(' + transformValue + 'deg)'}"
>
<audio
:src="src"
id="audio"
loop
></audio>
</div>
</div>
</template>
<script>
export default {
data () {
return {
src: 'http://o9o8lcfa3.bkt.clouddn.com/%E5%88%9A%E5%A5%BD%E9%81%87%E8%A7%81%E4%BD%A0.m4a',
playing: false,
transformValue: 10,
transformInterval: null
}
},
mounted () {
let trigger = document.getElementById('trigger')
let audio = document.getElementById('audio')
trigger.addEventListener('touchstart', () => {
audio.play()
this.togglePlay()
})

// 单独处理微信内置浏览器自动播放
document.addEventListener('WeixinJSBridgeReady', () => {
audio.play()
}, false)

// 模拟触发 「touchstart」 事件
this.createTouchstartEventAndDispatch(trigger)
},
methods: {
createTouchstartEventAndDispatch (el) {
let event = document.createEvent('Events')
event.initEvent('touchstart', true, true)
el.dispatchEvent(event)
},
togglePlay () {
this.playing = !this.playing
if (this.playing) {
this.transformInterval = setInterval(
() => this.changeTransformValue(),
100
)
} else {
clearInterval(this.transformInterval)
this.transformInterval = null
}
},
changeTransformValue () {
this.transformValue += 10
}
},
watch: {
playing (playing) {
let audio = document.getElementById('audio')
if (playing) {
audio.play()
} else {
audio.pause()
}
}
}
}
</script>
<style lang="less" scoped>
// var
@cdWidth: 50vmin;

// mixins
// 水平垂直居中的 flex 布局
.flex-align-justify-center {
display: flex;
align-items: center;
justify-content: center;
}

// 直径为 width 的圆
.circle (@width) {
width: @width;
height: @width;
border-radius: 50%;
}

// block 伪类
.absolute-block {
content: '';
display: block;
position: absolute;
}

.player {
.flex-align-justify-center;

width: 100vw;
height: 100vh;

&-cd {
.flex-align-justify-center;
.circle(@cdWidth);
background: linear-gradient(#666, #000);
transition: all .2s linear;

.absolute-circle-block (@color, @width) {
.absolute-block;
.circle(@width);
background: @color;
}

&::before {
.absolute-circle-block(darken(#f00, 5%), 20vmin);
}

&::after {
.absolute-circle-block(#222, 5vmin);
}
}
}
</style>

附上 Codepen 预览地址