最近刚好就做了音频播放器的需求,现将踩坑记录如右。
项目是基于React,镶嵌在页面。为此开发了组件audio.js。不过不管什么框架。逻辑都是一样的。
基础界面如下
相信布局方面已经没什么问题。组件结构如下:
<div style={{
paddingTop: '30px',
paddingLeft: 20,
paddingRight: 20,
border: '1px solid #ccc',
borderRadius: '4px'
}}>
<audio
preload="metadata"
src={src}
// autoPlay
ref={(audio) => {
this.lectureAudio = audio;
}}
style={{ width: '1px', height: '1px', visibility: 'hidden' }}
onCanPlay={() => this.handleAudioCanplay()}
onTimeUpdate={() => this.handleTimeUpdate()}
>
</audio>
{/* 进度条 */}
<div className="audio-control">
<div onClick={() => {
this.play()
}} className="audio-control-play">
<img src={!this.state.playState ? Img.play : Img.pause} alt="" />
</div>
</div>
<div className="audio-progress" ref={(r) => {
this.audioProgress = r
}}>
<div className="audio-progress-bar"
ref={(bar) => {
this.progressBar = bar
}}>
</div>
{/* 小点 */}
<div className="audio-progress-point-area" ref={(point) => {
this.audioPoint = point
}} style={{ left: this.state.left + 'px' }}>
<div className="audio-progress-point">
</div>
</div>
</div>
{/* 计时器 */}
<div className="audio-timer">
<p>{this.renderPlayTime(this.state.currentTime)}</p>
<p>{this.renderPlayTime(this.state.duration)}</p>
</div>
</div>
组件相关的样式如下:
/* 播放器相关代码 */
.audio-progress-bar{
width: 100%;
height: 4px;
background: rgb(195,195,195)
}
.audio-timer{
margin-top:10px;
display: flex;
justify-content: space-between;
}
/* 小点 */
.audio-progress-point-area{
width: 14px;
height: 14px;
border-radius: 50%;
background: rgb(80,172,81);
margin-top:-8px;
position: absolute;
left:0;
}
.audio-progress{
position: relative;
}
.audio-control{
height: 100px;
padding-top: 30px;
position: relative;
background: #fafafa;
}
.audio-control-play{
width: 30px;
cursor: pointer;
margin:10px auto
/* position: absolute;
top:30px; */
}
组件内部藏着一个audio。audio满足如下特殊属性
属性 | 描述 |
---|---|
currentTime(重要) | 设置或返回音频/视频中的当前播放位置(以秒计)。 |
duration | 返回当前音频/视频的长度(以秒计)。设置或返回是否在加载完成后随即播放音频/视频。 |
事件 | 描述 |
---|---|
canplay | 当浏览器可以开始播放音频/视频时触发。 |
ontimeupdate | 当currentTime更新时会触发timeupdate事件” |
pause | 当音频/视频已暂停时触发。 |
play | 当音频/视频已开始或不再暂停时触发。 |
playing | 当音频/视频在因缓冲而暂停或停止后已就绪时触发。 |
进度条的大致原理就是获取音频的当前播放时长以及音频总时长的比例,然后通过这个比例与进度条宽度相乘,可以得到当前播放时长下进度条需要被填充的宽度。
代码中的“audio-progress-bar-preload”是用来做缓冲条的,大概的做法也是一样,不过获取缓冲进度得用到audio的buffered属性,具体的用法推荐大家去MDN看看,在这里就不多赘述。
进度条以及播放按钮的布局代码大概就是这样,在css方面需要注意的就是进度条容器与进度条填充块以及进度条触点间的层级关系就好。
播放时,currntTime
是时刻变化的。那么如何监听呢?
进度控件自然是绝对定位的。
固然可以用定时器做。但是在网页性能不好的时候,定时器就是钱。前面提到ontimeupdate
事件回调。那真的是太好了。
handleTimeUpdate() {
let width = this.progressBar.offsetWidth;
this.setState(function (preState, props) {
let precentleft = (this.lectureAudio.currentTime / this.lectureAudio.duration);
return {
currentTime: this.lectureAudio.currentTime,
left: width * precentleft
}
});
}
在组件渲染结束后执行以下始化:
initListenTouch() {
this.audioPoint.addEventListener('touchstart', (e) => {
this.pointStart(e)
}, false);
this.audioPoint.addEventListener('touchmove', (e) => {
this.pointMove(e)
}, false);
this.audioPoint.addEventListener('touchend', (e) => {
this.pointEnd(e)
}, false);
this.progressBar.addEventListener('click', (e) => {
this.jump(e)
})
}
移动端处理有三个事件回调:
pointStart(e) {
e.preventDefault();
let touch = e.touches[0];
this.lectureAudio.pause();
//为了更好的体验,在移动触点的时候我选择将音频暂停
this.setState({
isPlaying: false,//播放按钮变更
startX: touch.pageX//进度触点在页面中的x坐标
});
}
pointMove(e) {
e.preventDefault();
let touch = e.touches[0];
let x = touch.pageX - this.state.startX; //滑动的距离
let maxMove = this.progressBar.clientWidth;//最大的移动距离不可超过进度条宽度
let offsetWindowLeft = this.progressBar.getBoundingClientRect().left
let moveX = this.lectureAudio.duration / this.progressBar.clientWidth;
//moveX是一个固定的常数,它代表着进度条宽度与音频总时长的关系,我们可以通过获取触点移动的距离从而计算出此时对应的currentTime
//下面是触点移动时会碰到的情况,分为正移动、负移动以及两端的极限移动。
if (x >= 0) {
// 一拖到底
if (x + this.state.startX - offsetWindowLeft >= maxMove) {
this.setState({
currentTime: this.state.duration,
left: offsetWindowLeft + this.progressBar.clientWidth
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
//改变audio真正的播放时间
})
// 正常前进
} else {
this.setState({
currentTime: (x + this.state.startX - offsetWindowLeft) * moveX
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
}
} else {
// 反向拖动
if (-x <= this.state.startX - offsetWindowLeft) {
// 反向托到底
this.setState({
currentTime: (this.state.startX + x - offsetWindowLeft) * moveX,
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
} else {
this.setState({
currentTime: 0
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
}
}
}
pointEnd(e) {
e.preventDefault();
if (this.state.currentTime < this.state.duration) {
this.touchendPlay = setTimeout(() => {
// this.handleAudioPlay();
this.setState({
playState: true
})
this.lectureAudio.play();
}, 300)
}
//关于300ms的setTimeout,一是为了体验的良好,可以试试不要300ms的延迟,会发现收听体验不好,音频的播放十分仓促。
//另外还有一点是,audio的pause与play间隔过短会出现报错,导致audio无法准确的执行相应的动作。
}
jump(e) {
let width = e.target.offsetWidth;
let x = e.offsetX
let currentTime = x / width * this.state.duration;
this.setState({
currentTime
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
}
### 其它逻辑
// 渲染秒为分钟
renderPlayTime(time) {
var minute = time / 60;
var minutes = parseInt(minute);
if (minutes < 10) {
minutes = "0" + minutes;
}
//秒
var second = time % 60;
var seconds = Math.round(second);
if (seconds < 10) {
seconds = "0" + seconds;
}
return `${minutes}:${seconds}`
}
play() {
if (this.state.playState == false) {
this.lectureAudio.play()
} else {
this.lectureAudio.pause()
}
this.setState((preState, props) => {
return {
playState: !preState.playState
}
})
}
整个组件用到的状态极少:
constructor(props) {
super(props);
this.state = {
currentTime: 0,
duration: 0,
left: 0,
playState: false
}
}
播放器的核心就是currentTime,这也是开发时的刻意为之,最后会发现这个组件中的唯一变量就是currentTime
,我们可以通过currentTime的变化完成所有的需求,并且不需要考虑其他因素的影响,因为所有的子组件都是围绕着currentTime运转。