需求背景
最近接到了一个需求,用户在播放音频的时候,有的音频长度过长,用户希望自主控制音频的进度和时长。但是微信提供原生的<audio>
的组件只能进行播放和暂停的控制,没有时长等元素,因此需要自己做一个播放器的组件。
技术背景
在需求调研之后,关于音频方面的API大概分为两种:
- audio相关的
createInnerAudioContext
:创建并返回一个innerAudioContext
对象。这个接口提供了一些音频相关的属性。 - 背景音乐相关的
getBackgroundAudioManager
:可以获取全局唯一的背景音频管理器backgroundAudioManager
。和audio类似,提供一些音频相关的方法和属性。
同时,微信从v1.6.3
起开放了自定义组件部分,支持简洁的组件化编程。想尝试一下组件化的开发模式,因此准备开发一个player组件
来代替audio标签
。同时低版本的基础库进行兼容操作。
技术选型
因为需要实现一个有独立作用域的单独组建,并且组建之间尽量不要互相影响。如果用bgm相关API的话需要维护一个全局的backgroundAudioManager
,因此选择audio相关的API。
同时因为采取了自定义组件,因此只需要考虑1.6.4版本的基础库即可,只需要wx.createInnerAudioContext()
来实现即可。
开发
之前开发过不少其他框架的组件,这次需要设计一个播放器,首先要定位播放器的功能。一个最重要的功能点:拖动控制音频播放进度。围绕着这一点,开始设计输入输出。
创建自定义组件
如官方文档描述,一个组件由json
、wxml
、 wxss
、js
四个文件组成。首先要在json中标注这是一个组件:
// player.json
{
"component": true
}
复制代码
在项目中进行应用时,同样在json文件中进行引用:
// page1.json
"usingComponents": {
"player": "component/player/player"
}
复制代码
即可在项目中类似常规组件的使用。
// page1.wxml
<player name="{{item.name}}" src="{{item.url}}"></player>
复制代码
在js部分,需要用Component
构造器构造一个实例来进行组建的管理,制定组件的属性、数据、方法等一些元素。
参数接收
输入最重要的就是音频的url了,其次是音频的其他信息,例如名称、作者、封面等等相关的部分。在组件中,定义对外属性要用properties
进行定义。
properties
属性是一个Object Map
,管理着所有的对外属性,包含三个字段:type
表示属性类型、 value
表示属性初始值、 observer
表示属性值被更改时的响应函数。
如上面的两个输入,即可在构造器中定义:
properties: {
// 这里定义了name和src属性,属性值可以在组件使用时指定
src: { // 属性名
type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个
observer: function(newVal, oldVal){} // 属性被改变时执行的函数(可选),也可以写成在methods段中定义的方法名字符串
},
name: String // 简化的定义方式
}
复制代码
其中需要注意的一点是,在properties
中定义的对外属性,可以在this.data
中直接调用,即为初始值。同时,可以用observer定义该值改动之后的回调,很好用。
在properties
属性中,属性名采用驼峰写法,调用时需要用连字符写法,数据绑定时需要用驼峰写法
作用域
在作用域部分,其实如果希望独立作用域的话,可以把一些实例和属性定义在Component
的data部分,这样不与其他组件进行共享,为私有数据进行模板渲染。在探索中,发现如果希望多组件共享属性的话,可以定义在组件外部,进行管理,这点仍需探索总结。
生命周期
自定义组件的生命周期有created
、attached
、ready
三种,家在顺序分别是created
→attached
→ready
。ready
时,页面基本布局完成,相当于onLoad,在这里可以进行一些组件的初始化操作。
另外,还有moved
和detached
两个生命周期,可以在detached
中定义一些注销方面的操作。
audio实例的预加载
因为组件内部定义了一个audio对象,需要在组件加载完毕之后进行初始化的操作,因此在ready
中进行实例的定义。
ready: function() {
let audioItem = wx.createInnerAudioContext()
audioItem.autoplay = true;
audioItem.loop = false;
audioItem.src = this.data.playUrl;
}
复制代码
这时定义了audioItem实例,并存储在data域中。
音频的长度获取
从需求出发,控制音频播放进度的前提是获取音频的长度。wx. createInnerAudioContext()
提供了对象duration
来获取音频的长度,但是有个前提,**音频必须加载之后才能获取该属性。**经过测试,所有事件回调中,只有onTimeUpdate
和onStop
事件才能成功的获取duration字段。具体如下:
audioItem.onTimeUpdate(function(){
let totalIndex = audioItem.duration;
let duration = second2minute(totalIndex);
that.setData({
totalIndex,
duration,
})
})
复制代码
但是onTimeUpdate事件会随着音频的加载一直触发,进而反复执行回调,因此采用定时器的方法,不断的轮询进行数据的加载。
let calcTimer = setInterval(function(){
calcIndex++;
// 反复尝试仍获取失败,则取消尝试
if(calcIndex>50) {
clearInterval(that.data.calcTimer);
}
if(audioItem.duration>0) {
let totalIndex = audioItem.duration;
let duration = second2minute(totalIndex);
that.setData({
totalIndex,
duration,
})
clearInterval(that.data.calcTimer);
}
},100)
复制代码
这算是一个实现方式,一般在1000ms之内可以获取总长度。
布局样式
首先看一下播放器的样式:
和自带的标签相比,主要是增加了一个进度条的部分。关于进度条的实现,用户需要来回拖动来进行音频进度的控制。
具体的实现,是用一个<slider>
来进行拖动的操作,但是原生的<slider>
会比较大,因此可以做一个伪装的进度播放来操作。
具体代码如下:
<view class="player">
<view class="player-poster">
<image mode="aspectFill" src="http://img.sharedaka.com/Fkg6nUUzd2bnigSz7vgViBFXrwLq" style="width: 100%;height: 100%;"/>
<view class="player-poster_button" bindtap="playMusic">
<image src="/component/player/image/play.png" style="width: 100%;height: 100%;" wx:if="{{!playState}}"/>
<image src="/component/player/image/stop.png" style="width: 100%;height: 100%;" wx:if="{{playState}}"/>
</view>
</view>
<view class="player-info">
<view class="player-info_title">{{playName}}</view>
<view class="player-info_time"></view>
<view class="player-info_control">
<text class="time">{{playtime}}</text>
<progress percent="{{downloadPercent}}" color="#E4E4E4" stroke-width="2" class="player-info_process">
<text class="playstate" style="left:{{percent}}%"></text>
<text class="playstate-outer" style="left:{{percent}}%"></text>
<text class="dpstate" style="width:{{percent}}%"></text>
<slider disabled="{{duration === '00:00'}}" class="slider" bindchanging="changeSeek" color="#d33a31" left-icon="cannel" value="{{percent}}"></slider>
</progress>
<text class="time">{{duration}}</text>
</view>
</view>
</view>
复制代码
具体操作
播放器中主要涉及两个操作:
- 音频的播放和暂停。
- 音频进度条的拖动效果。
音频播放及暂停
在音频播放时,及触发methods中的playMusic()
方法。
首先获取目前播放器的状态及实例:
let playState = this.data.playState;
let audioItem = this.data.audioItem;
复制代码
当播放器在未播放状态下时,执行操作,并开始计时器的计算:
audioItem.play();
let timer = setInterval(function() {
let timeIndex = ++that.data.timeIndex;
if(timeIndex > that.data.totalIndex) {
clearInterval(that.data.timer);
that.setData({
timeIndex: 0,
percent: 0,
playtime: '00:00',
playState: false
})
audioItem.stop();
return;
}
let playtime = second2minute(timeIndex);
that.setData({
timeIndex: timeIndex,
playtime,
percent: timeIndex/that.data.totalIndex*100
});
}, 1000)
复制代码
当播放器在播放状态下,执行操作:
audioItem.pause();
clearInterval(this.data.timer);
复制代码
记得对计时器的存储以及播放器状态的保存
播放器进度调整
当拖动播放器进度时,可以利用bindchanging
来进行控制。这时需要计算当前进度是音乐总时长的占比,并跳转到相应的位置播放。
// 调整位置
changeSeek: function(e) {
let value = e.detail.value;
let nowIndex = Math.floor(totalIndex * value/100);
if(playState) {
audioItem.seek(nowIndex);
}
that.setData({
percent: e.detail.value,
timeIndex: nowIndex,
})
}
复制代码
到此,即完成了player
播放器的整体开发工作。
兼容性处理
前面说到,目前自定义组件只针对1.6.4以上的用户可以使用。因此需要对以下的版本进行兼容性的操作。
首先在js中判断当前用户基础库的版本:
wx.getSystemInfo({
success: function(res) {
let sdk = res.SDKVersion;
if(sdk > '1.6.3') {
that.setData({
canUsePlayer: true
})
}
}
})
复制代码
用canUsePlayer
来进行版本的标记。在项目中,根据标记选用不同的组件。
<audio name="{{item.extra}}" src="{{qiNiuUrl + item.content}}" wx:if="{{!canUsePlayer}}"></audio>
<player name="{{item.extra}}" src="{{qiNiuUrl + item.content}}" wx:if="{{canUsePlayer}}"></player>
复制代码
尾巴
to be continue
近期还有一个新的需求,需要音乐在小程序收起的时候继续播放。估计马上就要踩坑getBackgroundAudioManager
了。页面维护一个公用的实例。
小感悟
从最近的一些列新feature可以看出,微信小程序的生态要逐步完善了。分包、直播、组件的开放说明微信小程序的可玩性页逐渐提高。
试水自定义组件的开发,感觉目前的开发模式还是比较简单的,相较于之前用template
的模式更加方便,而且还有一些组件之间复用的新特性relations
等组件之间的关系,可玩性很高。
估计不久之后就有一套第三方组件支持小程序的各项功能。