iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
0
Modern Web

Web x Sound - 用 Web 玩轉聲音系列 第 5

Day05 - 使用 HTML 播放音檔 - 自製播放器 (2)

  • 分享至 

  • xImage
  •  

今天就開始刻自製播放器的功能吧!

準備資料

第一步就是先準備音樂、專輯資訊、專輯圖片等。

原先想串現有的 API,像是 KKBOX Open APISoundCloud APISpotify 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',
	},
];

功能實作

程式初步的規劃大致如下:

  1. AudioPlayer 模組:負責處理所有播放器邏輯
  2. 事件驅動的 UI:監聽 Media & 自訂 Event 以顯示對應 UI
  3. 多媒體資料來源:前一部份的物件

雖然有使用 jQuery ,但為了將 UI 顯示與狀態綁定 (雖然是手動的),採取 event driven 作法,這麼做比起直接操作修改 DOM 更簡單、不容易錯。(好想用 React 寫啊~~~~)

AudioPlayer

這邊採用 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;
	}
}

CodePen Demo

那麼明天再來繼續完成 UI 串接的部分~

Reference


上一篇
Day04 - HTMLMediaElement
下一篇
Day06 - 使用 HTML 播放音檔 - 自製播放器 (3)
系列文
Web x Sound - 用 Web 玩轉聲音13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言