@DIYgod/APlayer 是一款簡潔漂亮的 HTML5
音樂播放器 (〃ノωノ)
在我第一次看到這款播放器顏值的時候讓我眼前一亮,我很是崇拜那些能設計出好看界面的設計師 (* >ω<)css
可是在用過以後發現仍是有不足的地方 這是我曾經提過的 Issuesvue
用了一段時間,很喜歡 APlayer 簡潔的 UI,提一些其餘可改進的建議:react
1.我認爲有必要提供動態管理播放列表的 API
(若是沒有,在須要動態添加歌曲到列表時只能從新初始化)
2.應該提供一個銷燬播放器的 API
3.歌詞容許異步添加,一般獲取歌詞接口是單獨的
(如今必須等待歌詞接口返回再初始化播放器,若歌詞獲取失敗或時間過長會同時影響到播放音樂功能)webpack
關於第三條,APlayer 實際上是支持異步歌詞的但僅支持傳入 .lrc
文件的地址
若是是像網易雲/QQ音樂那樣返回的是 JSON
格式的那就不知足需求了ios
爲何不提 PR 要重寫呢?
這個我想了一會,最終仍是以爲組件化的方式開發更好一些(原 APlayer 用的是原生 JS 沒有依賴別的庫)
並且由於我之前還在作後端的時候就本身寫過音樂播放器(仿微博播放器,當時不會用 Git
源碼已丟)
因此挺有經驗的,重寫一個難度也不大,並且比較爲所欲爲,還能夠隨意加一些本身想要的東西 qwqgit
說明:該播放器是基於 @DIYgod/APlayer 的佈局和 樣式 採用 TS + Vue 組件化重構的github
演示:http://aplayer.quq.cat
文檔:http://aplayer.quq.cat/docs
源碼:https://github.com/MoeFE/vue-...
NPM:https://www.npmjs.com/package...
播放列表來自網易雲歌單:http://music.163.com/#/playli... web
若是喜歡的話別忘了點一個 star 喲 (*ゝω・)
歡迎提 Issues 和 PR (´・_・`)正則表達式
爲了你們使用方便,我選擇 Vue ,能夠響應式控制播放器各個屬性 並以插件的形式發佈(詳情請看 demo )
我這裏爲了方便你們能更好的調試,在生產環境下開啓了 SourceMap
和 devtools
若是您安裝了 vue-devtools 能夠打開調試器查看組件劃分和各個組件的信息typescript
至於爲何選擇用 TypeScript
本文就不作過多的解釋了
你們能夠自行在網上找一下 TypeScript
和 JavaScript
的區別
我只能告訴你:對於一個曾經使用 C#
的開發者來講這簡直不能太爽啦 微軟爸爸賽高 (* >ω<)
最後推薦一款 TS + Vue 的腳手架模版:https://github.com/Toilal/vue...
之後或將加入到官方模版中:https://github.com/vuejs-temp...
TS + React 的腳手架能夠用這個:https://github.com/wmonk/crea...
拿到佈局樣式後要作的第一件事情就是拆分組件
將 @DIYgod/APlayer 的佈局和 樣式 複製過來
確保樣式沒有問題後再將各個組件的佈局和樣式單獨複製出來
不懂設計的只好複製了 請容許我作一個悲傷的表情 (ಗ ‸ ಗ )
我將播放器拆分紅了如下組件:
組件名稱 | 組件說明 |
---|---|
APlayer.ts | 播放器容器組件 |
Button.ts | 按鈕組件 |
Picture.ts | 歌曲圖片組件 |
Container.ts | 右側容器組件 |
Info.ts | 歌曲信息組件 |
Lyric.ts | 歌詞面板組件 |
Progress.ts | 進度條組件 |
Time.ts | 播放時間組件 |
Volume.ts | 音量控制組件 |
List.ts | 播放列表組件 |
Item.ts | 播放列表項組件 |
再來一張更清晰的圖片吧:
功能開發其實沒有多少難度,HTML5
已經封裝好了 HTMLAudioElement 元素
咱們就是用一下它的 API
和視圖進行數據綁定和交互而已 看一下文檔就行了
不過這裏會遇到一個小問題,那就是 Vue 不能監聽到 Audio
對象的屬性變化
由於 Audio
對象其實就是 HTMLAudioElement 元素,Vue 是不能監聽到元素屬性變化的,因此我想了一個小辦法
定義了一個 Media
接口,裏面定義了和 Audio
對象相同的屬性,在 Audio
的事件中對 Media
的屬性進行同步
這樣的話,就可使用 Media
對象響應式獲取 Audio
的屬性值
能夠查看這一段代碼:APlayer.ts#L326-L334
我這裏簡單介紹一些比較經常使用的屬性和方法吧
名稱 | 說明 |
---|---|
autoplay |
是否自動播放 (在 Safari 中無效,能夠自行在初始化音頻後手動調用 play 方法) |
bufferd |
獲取已緩衝的進度(必須在 readeyState >= 3 以後獲取,不然會拋異常) |
loop |
是否循環播放音頻(推薦根據當前播放模式自行實現該功能) |
preload |
預加載選項,推薦使用 metadata ,在未播放時僅獲取音頻的長度,而不要加載整個音頻 |
src |
獲取或設置音頻的播放地址 |
volume |
獲取或設置音頻的音量(0~1) |
paused |
獲取當前音頻是否已暫停 |
currentTime |
獲取或設置當前音頻的播放進度(單位:秒) |
duration |
獲取當前音頻的長度(單位:秒) |
playbackRate |
獲取或設置當前音頻的播放速度 |
play () |
播放音頻 |
pause () |
暫停音頻 |
事實上 Audio
和 Video
對象差很少 都屬於 Media
因此若是你會開發音樂播放器那麼也會開發視頻播放器了
這裏重點說一下 timeupdate
事件,這個事件在音頻播放時不斷觸發,這個能夠說是最有用的事件了
由於在播放過程當中須要不斷的重繪播放器的播放進度和已播放時間
若是有歌詞的話,還須要根據當前的播放時間去同步歌詞
若是沒有或者不知道這個事件的話,你可能會使用 setInterval
代替
使用 setInterval
的話,會有兩個問題:
1.重繪時間到底設置多少合適?太快了影響性能,太慢了頁面不一樣步
2.若是用戶暫停播放了,須要清除定時器,開始播放又要初始化定時器,太麻煩
(或者偷懶的話能夠判斷 paused
時 return
,那麼須要不斷的跑一個空定時器)
可能作這個功能的時候是最好玩的吧 qwq
由於在好久之前 千千靜聽那個年代 我無聊的時候就作一下 LRC 歌詞
因此對這個功能很敏感 儘可能作到最好吧 (´・_・`)
這裏主要功能是歌詞解析,歌詞同步的話只要計算出與當前播放時間最匹配的項元素
而後設置歌詞面板的滾動條位置到當前元素的位置便可
常見的時間標籤有如下幾種
[mm:ss] 只有分和秒的時間標籤 [mm:ss:ms] 有分、秒、毫秒的時間標籤 [mm:ss.ms] 有分、秒、毫秒的時間標籤的另外一種格式 [mm:ss:ms][mm:ss.ms] 多個時間標籤共享這一句相同的歌詞
個人思路是:
首先按照行將歌詞文本分割成數組,再按行進行解析
使用正則表達式匹配出該行的 分、秒、毫秒 和顯示的歌詞文本
將 分、秒、毫秒 都轉換成毫秒單位而後加起來與歌詞文本關聯後保存到數組中,最後須要按照時間正序排列
那麼 當前要顯示的歌詞 = 過濾數組中 時間 < 當前播放時間 後的最後一項
private async parseLRC (): Promise<void> { if (!this.lrc || this.lrc === 'loading') return if (this.isURL(this.lrc)) { // 若是歌詞是一個URL地址則請求該地址得到歌詞文本 const { data } = await Axios.get(this.lrc.toString()) this.currentLRC = data } else this.currentLRC = this.lrc const reg = /\[(\d+):(\d+)[.|:](\d+)\](.+)/ const regTime = /\[(\d+):(\d+)[.|:](\d+)\]/g const regCompatible = /\[(\d+):(\d+)]()(.+)/ const regTimeCompatible = /\[(\d+):(\d+)]/g const regOffset = /\[offset:\s*(-{0,1}\d+)\]/ const offsetMatch = this.lrc.match(regOffset) const offset = offsetMatch ? Number.parseInt(offsetMatch[1]) : 0 this.LRC = [] const matchAll = (line: string) => { let match = line.match(reg) || line.match(regCompatible) if (!match) return if (match.length !== 5) return const minutes = Number.parseInt(match[1]) || 0 const seconds = Number.parseInt(match[2]) || 0 const milliseconds = Number.parseInt(match[3]) || 0 const time = (minutes * 60 * 1000 + seconds * 1000 + milliseconds) + offset const text = (match[4] as string).replace(regTime, '').replace(regTimeCompatible, '') if (!text) return // 優化:不要顯示空行 this.LRC.push({ time, text }) matchAll(match[4]) // 遞歸匹配多個時間標籤 } this.currentLRC.replace(/\\n/g, '\n').split('\n').forEach(line => matchAll(line)) // 歌詞格式不支持 if (this.LRC.length <= 0) this.LRC = [{ time: -1, text: '(・∀・*) 抱歉,該歌詞格式不支持' }] else this.LRC.sort((a, b) => a.time - b.time) }
完善了原 APlayer 不足的地方:
1.能夠響應式的隨意控制播放器屬性
2.歌詞同步支持多種時間標籤格式(fix #39)
3.歌詞同步兼容 [offset:0]
標籤
4.異步歌詞的支持
5.容許控制播放速度(相同的歌曲用不一樣的速度聽感受會不同哦 quq)
6.音量容許拖動控制
7.支持註冊全部 Media
事件
8.保存播放器配置到 localStorage
中,刷新後能夠恢復播放進度等信息
最後 弱弱的:@MoeFE 歡迎各位大佬加入 (๑•̀ㅂ•́)و✧
額..沒啥要求 頭像要萌要可愛的!!
好想有個大佬能帶我裝逼帶我飛 (ง •_•)ง