相信大家都有過開發音樂播放器的需求,我也不例外,最近剛好就做了一個這樣的需求,所以就順便把遇到的問題和踩過的坑,分享一下~
因為專案是基於React,所以下面的原始碼基本都是在播放器元件中抽出來的,不過無論在什麼框架下開發,邏輯是一樣滴。
播放器皮膚
先從簡單的開始講起吧,說到播放器幾個常用的操作按鈕肯定少不了,但是最重要的是那一個可以拖動的播放條。
<!--進度條begin-->
<div className="audio-progress" ref={(r) => { this.audioProgress = r }}>
<div className="audio-progress-bar" ref={(bar) => { this.progressBar = bar }}>
<div className="audio-progress-bar-preload" style={{ width: bufferedPercent + '%' }}></div>
<div className="audio-progress-bar-current" style={{ width: `calc(${currentTime}/${currentTotalTime} * ${this.progressBar && this.progressBar.clientWidth}px)` }}></div>
</div>
<div className="audio-progress-point-area" ref={(point) => { this.audioPoint = point }} style={{ left: left + 'px' }}>
<div className="audio-progress-point">
</div>
</div>
</div>
<!--進度條begin-->
<div className="audio-timer">
<p>{this.renderPlayTime(currentTime)}</p>
<p>{this.renderPlayTime(currentTotalTime)}</p>
</div>
<div className="audio-control">
<div className="audio-control-pre">
<img src={PreIcon} alt="" />
</div>
<div className="audio-control-play">
<img src={Play} alt="" />
</div>
<div className="audio-control-next">
<img src={NextIcon} alt="" />
</div>
</div>
複製程式碼
進度條的大致原理就是獲取音訊的當前播放時長以及音訊總時長的比例,然後通過這個比例與進度條寬度相乘,可以得到當前播放時長下進度條需要被填充的寬度。
程式碼中的“audio-progress-bar-preload”是用來做緩衝條的,大概的做法也是一樣,不過獲取緩衝進度得用到audio的buffered屬性,具體的用法推薦大家去MDN看看,在這裡就不多贅述。
進度條以及播放按鈕的佈局程式碼大概就是這樣,在css方面需要注意的就是進度條容器與進度條填充塊以及進度條觸點間的層級關係就好。
播放器功能邏輯
也許看了上面,大家覺得很疑惑,為什麼沒有見到最關鍵的audio標籤。那是因為,其實整個播放器的邏輯重點都在那個標籤裡,我們需要單獨抽出來分析一下。
<audio
preload="metadata"
src={src}
ref={(audio) => {this.lectureAudio = audio}}
style={{width: '1px', height: '1px', visibility: 'hidden'}}
onCanPlay={() => this.handleAudioCanplay()}
onTimeUpdate={() => this.handleTimeUpdate()}>
</audio>
複製程式碼
① 怎麼讓進度條動起來?
解決思路--音訊在播放的時候,當前進度(currentTime)是一直都在變化的,也就是說,能找到一個將currentTime變化具現為進度條長度的常數是關鍵。說的這麼複雜,其實在上面的佈局程式碼裡已經劇透了,
calc(${currentTime}/${currentTotalTime} * $this.progressBar.clientWidth}px
複製程式碼
這樣就能很輕鬆的算出當前進度需要對應的進度條填充長度。問題又來了,我知道進度條該填充多長了,但是它還是不會動額... 在這裡,我們有兩個方法可以解決:
- 利用setInterval 我們可以每隔300ms就檢測一下當前audio的currentTime,然後在setState動態改變state中的currentTime,接著元件便會重渲染進度條部分的展示,從而也就讓我們的進度條動起來了。
- 利用audio的ontimeupdate事件 這個是audio和video都擁有的原生事件,作用是--“當currentTime更新時會觸發timeupdate事件”,一般推薦使用這種方式來動態計算進度條的寬度,畢竟可以少寫一個計時器,說不定可以規避一些專案中的隱患。而且這是HTML的原生事件,瀏覽器的支援肯定是充分的,所以從效能的角度來說應該是比上一種方式要好。
ontimeupdate時執行的方法--每次觸發該事件時都重新給currentTime賦值,剩下的改動都可以通過currentTime值的變化而做出相應的變化。
handleTimeUpdate() {
if (this.state.currentTime < (this.state.currentTotalTime - 1)) {
this.setState({
currentTime: this.lectureAudio.currentTime
});
} else {
......
}
}
複製程式碼
② 怎麼在移動端拖動進度條?
解決思路--既然是移動端的觸碰事件,那麼touch事件自然就是主角了。通過touch事件我們可以計算出,拖動的距離,進而得到進度條以及觸點該移動的距離。
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);
}
複製程式碼
這是元件載入時掛在到進度條觸點的三個事件監聽,這裡講一下三個監聽具體都有些什麼作用。
- touchstart--負責獲取觸控進度觸點時觸點的方位
pointStart(e) {
e.preventDefault();
let touch = e.touches[0];
this.lectureAudio.pause();
//為了更好的體驗,在移動觸點的時候我選擇將音訊暫停
this.setState({
isPlaying: false,//播放按鈕變更
startX: touch.pageX//進度觸點在頁面中的x座標
});
}
複製程式碼
- touchmove--負責動態計算觸點的拖動距離,並轉換成this.state.currentTime從而觸發元件的重渲染.
pointMove(e) {
e.preventDefault();
let touch = e.touches[0];
let x = touch.pageX - this.state.startX; //滑動的距離
let maxMove = this.progressBar.clientWidth;//最大的移動距離不可超過進度條寬度
//(this.state.moveX) = this.lectureAudio.duration / this.progressBar.clientWidth;
//moveX是一個固定的常數,它代表著進度條寬度與音訊總時長的關係,我們可以通過獲取觸點移動的距離從而計算出此時對應的currentTime
//下面是觸點移動時會碰到的情況,分為正移動、負移動以及兩端的極限移動。
if (x >= 0) {
if (x + this.state.startX - this.offsetWindowLeft >= maxMove) {
this.setState({
currentTime: this.state.currentTotalTime,
//改變當前播放時間的數值
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
//改變audio真正的播放時間
})
} else {
this.setState({
currentTime: (x + this.state.startX - this.offsetWindowLeft) * this.state.moveX
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
}
} else {
if (-x <= this.state.startX - this.offsetWindowLeft) {
this.setState({
currentTime: (this.state.startX + x - this.offsetWindowLeft) * this.state.moveX,
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
} else {
this.setState({
currentTime: 0
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
})
}
}
}
複製程式碼
- touchend--負責恢復音訊的播放
pointEnd(e) {
e.preventDefault();
if (this.state.currentTime < this.state.currentTotalTime) {
this.touchendPlay = setTimeout(() => {
this.handleAudioPlay();
}, 300)
}
//關於300ms的setTimeout,一是為了體驗的良好,大家在做的時候可以試試不要300ms的延遲,會發現收聽體驗不好,音訊的播放十分倉促。
//另外還有一點是,audio的pause與play間隔過短會出現報錯,導致audio無法準確的執行相應的動作。
}
複製程式碼
③ 怎麼實現列表播放與迴圈播放?
解決思路--時刻關注currentTime與duration之間的關係
handleTimeUpdate() {
if (this.state.currentTime < (this.state.currentTotalTime - 1)){
......
}else {
//此情況為音訊已播放至最後
if (this.state.isLooping){//是否為迴圈播放
//currentTime歸零,並且手動將audio的currentTime設為0,並手動執行play()
this.setState({
currentTime: 0
}, () => {
this.lectureAudio.currentTime = this.state.currentTime;
this.lectureAudio.play();
})
}else {
//列表播放則只需判斷是否有下一首,有則跳轉或播放,無則暫停播放。
if (this.props.audioInfo.next_lecture_id && this.state.currentTime !== 0){
this.handleNextLecture();
}else {
this.handleAudioPause();
}
}
}
}
複製程式碼
總結
不知道大家看完之後有沒一種感覺,好像無論是什麼都離不開this.state中的currentTime。
是的,這個播放器的核心就是currentTime,這也是開發時的刻意為之,最後我們會發現這個元件中的唯一變數就是currentTime,我們可以通過currentTime的變化完成所有的需求,並且不需要考慮其他因素的影響,因為所有的子元件都是圍繞著currentTime運轉。
以上是我關於播放器開發的一點小心得,雖然沒有十分酷炫的皮膚,也沒有十分複雜的功能,但是對個人而言,它很好的幫我理清了一個[可用的]播放器的開發流程。