今天就開始刻自製播放器的功能吧!
第一步就是先準備音樂、專輯資訊、專輯圖片等。
原先想串現有的 API,像是 KKBOX Open API、SoundCloud API、Spotify API...等,雖然可以取得專輯資訊,但是音樂還是得用他們的播放器或 SDK 方法播放,沒辦法直接取得 url 或原始串流 (畢竟有版權保護)。
為了讓範例簡單一點,這邊採用 Youtube Audio Library 提供的免費音樂,搭配 flickr無版權的圖做為專輯封面,建立簡單的歌曲資訊。
首先把音樂、圖片檔案丟到 AWS S3 上,建立一個物件儲存歌曲資訊,就完成準備資料了!
const mediaData = [
{
author: 'Freedom Trail Studio',
authorUrl: 'https://www.youtube.com/channel/UCx6kpgiQkDkN1UnK5GaATGw',
fileName: 'Swing Theory',
fileUrl: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/Swing_Theory.mp3',
thumb: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/15367448967_0551dce9c1_q.jpg',
},
{
author: 'Huma-Huma',
authorUrl: '',
fileName: "It's All Happening",
fileUrl: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/It_s_All_Happening.mp3',
thumb: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/34347642316_fe2f354cfd_q.jpg',
},
{
author: 'Danny Kean/Doug Maxwell',
authorUrl: 'https://www.youtube.com/channel/UCwhJTv7O8EmDwyvqMBLHcHg',
fileName: "So Smooth",
fileUrl: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/So_Smooth.mp3',
thumb: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/36981460496_80c2c2bce5_q.jpg',
},
{
author: 'Silent Partner',
authorUrl: '',
fileName: "Sinking Ship",
fileUrl: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/Sinking_Ship.mp3',
thumb: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/38552225096_84b69eb7aa_q.jpg',
},
{
author: 'Jimmy Fontanez/Doug Maxwell',
authorUrl: 'https://www.youtube.com/channel/UCwhJTv7O8EmDwyvqMBLHcHg',
fileName: "Trap Unboxing",
fileUrl: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/Trap_Unboxing.mp3',
thumb: 'https://s3-ap-northeast-1.amazonaws.com/dazedbear-assets/custom-audio-player/41451305061_f0bd9717be_q.jpg',
},
];
程式初步的規劃大致如下:
雖然有使用 jQuery ,但為了將 UI 顯示與狀態綁定 (雖然是手動的),採取 event driven 作法,這麼做比起直接操作修改 DOM 更簡單、不容易錯。(好想用 React 寫啊~~~~)
這邊採用 ES6 class 語法,設計介面時會相對直覺與簡單。
class AudioPlayer {
constructor(playlist) {
this.audioPlayer = new Audio();
this.playList = playlist || [];
this.playMode = 'step';
this.currentIdx = 0;
this.isPlaying = false;
// internal: volume
// internal: duration
// internal: currentTime
this.initEvents();
this.setCurrentMusic();
}
// 內部註冊的事件
initEvents() {
// 建立自訂事件
this.event = {
current: new Event("currentmusicchange"),
playMode: new Event("playmodechange"),
playList: new Event("playlistchange"),
isPlaying: new Event("playstatuschange"),
}
// 讀取完成自動播放
this.audioPlayer.addEventListener('canplay', () => this.isPlaying ? this.audioPlayer.play() : this.audioPlayer.pause())
// 播放狀態
this.audioPlayer.addEventListener('play', () => this.setPlayStatus(true))
// this.audioPlayer.addEventListener('pause', () => this.setPlayStatus(false))
this.audioPlayer.addEventListener('abort', () => this.setPlayStatus(false))
this.audioPlayer.addEventListener('error', () => this.setPlayStatus(false))
// this.audioPlayer.addEventListener('emptied', () => this.setPlayStatus(false))
this.audioPlayer.addEventListener('stalled', () => this.setPlayStatus(false))
// 自動播下一首
this.audioPlayer.addEventListener('ended', () => {
const nextIdx = this.getNextMusicIdx();
this.setCurrentMusic(nextIdx);
if (this.playMode === 'step' && nextIdx === 0) {
this.togglePlay(false);
}
})
// 換歌時讀到歌曲總時間才算完成更換
this.audioPlayer.addEventListener('durationchange', () => {
this.audioPlayer.dispatchEvent(this.event.current)
})
}
// 對外暴露的註冊事件 callback
// internal event: timeupdate, volumechange, durationchange
// custom event: playmodechange, currentmusicchange, playlistchange, playstatuschange
on(event, callback) {
this.audioPlayer.addEventListener(event, callback);
}
// 指定並讀取當下要播放的音樂
// identifier = prev, next, 0, <integer>
setCurrentMusic(identifier = this.currentIdx) {
if (Number.isInteger(identifier)) {
if (identifier < 0 || identifier >= this.playList.length) return;
this.currentIdx = identifier;
} else if (typeof identifier === 'string') {
const newIdx = this.getNextMusicIdx(identifier);
if (!newIdx) return;
this.currentIdx = newIdx;
} else {
console.error('不合法的 identifier in setCurrentMusic');
return;
}
this.audioPlayer.setAttribute('src', this.getMediaInfo().fileUrl)
this.audioPlayer.load()
}
// 取得指定的歌曲資訊
getMediaInfo(idx = this.currentIdx) {
if (!Number.isInteger(idx) || idx < 0 || idx >= this.playList.length) return {};
return this.playList[this.currentIdx];
}
// 播放 / 暫停
togglePlay(nextIsPlaying = !this.isPlaying) {
if (nextIsPlaying) {
// 播放
this.audioPlayer.play()
.then(() => this.setPlayStatus(true))
.catch(e => console.error('播放發生錯誤', e))
} else {
// 暫停
this.audioPlayer.pause()
this.setPlayStatus(false);
}
}
// 調整播放模式
setPlayMode(mode) {
const validMode = ['step', 'shuffle', 'repeat-one', 'repeat-all'];
if (validMode.indexOf(mode) !== -1) {
this.playMode = mode;
this.audioPlayer.dispatchEvent(this.event.playMode);
}
}
// 調整播放狀態
setPlayStatus(val) {
if (typeof val !== 'boolean') return;
this.isPlaying = val;
this.audioPlayer.dispatchEvent(this.event.isPlaying);
}
// 取得下一首要播放的歌曲
getNextMusicIdx(operation = this.playMode) {
let nextIdx = 0;
switch (operation) {
case 'step':
// 到播放清單底,結束播放
if ((this.currentIdx + 1) >= this.playList.length) this.audioPlayer.pause();
nextIdx = (this.currentIdx + 1) >= this.playList.length ? 0 : this.currentIdx + 1;
break;
case 'next':
case 'repeat-all':
nextIdx = (this.currentIdx + 1) >= this.playList.length ? 0 : this.currentIdx + 1;
break;
case 'prev':
nextIdx = (this.currentIdx - 1) < 0 ? (this.playList.length - 1) : (this.currentIdx - 1);
break;
case 'shuffle':
nextIdx = Math.floor(Math.random()*this.playList.length - 1);
break;
case 'repeat-one':
nextIdx = this.currentIdx;
break;
default:
console.log('不合法的操作', operation);
return;
}
return nextIdx;
}
// 音量調整 (0 - 100)
setVolume(vol) {
if (typeof vol !== 'number') return;
this.audioPlayer.volume = vol / 100;
}
// 進度條調整
setCurrentTime(nextSec) {
const currentMusic = this.getMediaInfo();
if (!currentMusic) return;
if (nextSec > currentMusic.duration) {
this.audioPlayer.currentTime = 0;
} else if (nextSec < 0) {
this.audioPlayer.currentTime = 0;
} else {
this.audioPlayer.currentTime = nextSec;
}
}
// 取得當前音量
getVolume() {
return this.audioPlayer.volume;
}
// 取得當前秒數
getCurrentTime() {
return this.audioPlayer.currentTime;
}
// 取得當前歌曲總長度
getDuration() {
return this.audioPlayer.duration;
}
// 取得當前播放模式
getPlayMode() {
return this.playMode;
}
// 取得當前播放狀態
getIsPlaying() {
return this.isPlaying;
}
}
那麼明天再來繼續完成 UI 串接的部分~