昨天把播放器切版完成了 (灑花),不過在實作功能以前,先來談談一些基礎知識吧!
const audioPlayer = document.createElement('audio');
if (audioPlayer.canPlayType('audio/mpeg')) {
audioPlayer.setAttribute('src','audiofile.mp3');
}
audioPlayer.play();
audioPlayer.addEventListener('ended', () => console.log('audiofile play ended'));
<audio> 除了提供內建的播放器,我們也可以自己刻一個播放器,再使用 JavaScript 操作 <audio> ,幫我們在背景實現播放功能,對於需求單純、不牽涉到自適應 Stream 等情境就已足夠了。
我們使用 document 新建、取得的 <audio> element ,實際上是一個 HTMLAudioElement,它是 HTMLMediaElement 的子類別,HTMLMediaElement 提供了一些方法與事件以便操作 element :
| method & property | description | parameters | return |
|---|---|---|---|
| load() | 中斷所有進行中的事件,重置到初始狀態,重新進行音源選擇、讀取、準備從頭播放 | none | undefined |
| play() | 開始播放 (resolve: 開始播放 / reject: 其他理由) | none | Promise |
| pause() | 暫停播放 | none | none |
| canPlayType() | 檢測瀏覽器是否支援該檔案格式 (MIME type) | MIME type 字串, ex: 'audio/mpeg' | "probably" / "maybe" / "" |
| captureStream() | 取得即時串流 | none | MediaStream object |
| currentTime | 取得、設定當前播放秒數 (sec) | ||
| volume | 取得、設定當前音量 (0 - 1) |
load 實際上是重置播放器,並不是一個決定要載入哪一支音源的方法。想要設定音源路徑、決定音源的載入順序,就得使用 src 屬性與 <source> 標籤。
// src
const audioPlayer = document.createElement('audio');
audioPlayer.setAttribute('src', 'audiofile.mp3');
audioPlayer.load();
// source
const audioPlayer = document.createElement('audio');
const source1 = document.createElement('source')
.setAttribute('src', 'audiofile.wav')
.setAttribute('type', 'audio/wav');
const source2 = document.createElement('source')
.setAttribute('src', 'audiofile.mp3')
.setAttribute('type', 'audio/mpeg');
audioPlayer.appendChild(source1);
audioPlayer.appendChild(source2);
audioPlayer.load();
至於 load 會做哪些事情呢?
| 順序 | element 現況 | 處理 | 觸發的事件 |
|---|---|---|---|
| 1 | 讀取音源中 | 中斷讀取 | abort |
| 1 | 已完成讀取音源 | 清空 buffer | emptied |
| 2 | 時間軸位置不在一開始 | 移動到最前面 | timeupdate |
| 3 | 完成初始化 | 重新 scan、select、load 音源 | loadstart |
| 4 | 後面與一般 Media Event 順序相同 |
這個方法用來詢問瀏覽器是否支援此檔案格式的播放,回傳值是字串。
| value | description |
|---|---|
| 'probably' | 此格式看起來可以播放 |
| 'maybe' | 不確定,要試播才知道 |
| '' | 看起來不行播放 |
第一眼看到的時候不小心笑了,要試試看才知道!? 不曉得有什麼典故,竟然有這種曖昧不明的回應... XD
play 會回傳 Promise (舊版本瀏覽器不會回傳值),當 resolve 就代表開始播放,其他各種原因導致無法播放就會 reject。
const audioPlayer = document.createElement('audio').setAttribute('src', 'audiofile.mp3');
const status = doument.getElementById('player-status');
const btn = doument.getElementById('player-btn');
audioPlayer.load();
audioPlayer.play()
.then(() => {
btn.className = 'pause';
status.innerHTML = 'Playing';
})
.catch(e => {
console.error(e);
btn.className = 'play';
status.innerHTML = 'Stop';
});
至於 Promise reject 的情況,常見的有
| error | description |
|---|---|
| NotAllowedError | 瀏覽器或 OS 不允許播放,像是不允許背景自動播放 |
| NotSupportedError | 不支援此檔案格式 |
一般建議先用 canPlayType() 檢查是否支援再播放,以節省瀏覽器 request 數量與流量。
另外,想要取得、設定當前播放的位置,play() 沒辦法傳參數作控制,需要使用 currentTime 來處理。
// get
console.log(audioPlayer.currentTime);
// set
audioPlayer.currentTime = 121;
除了上述的方法以外,我們也可以監聽特定事件來作更細微的處理。
事件分成兩類:Loading 與 Playing。
這些是音源在讀取過程中會觸發的事件,監聽 Loading 事件可以幫助我們在 render 、enable/disable 播放器有更精準的控制。
| trigger order | event | description |
|---|---|---|
| 1 | loadstart | 一開始讀取程序 |
| 2 | durationchange | 讀取部分 metadata,包含音檔總時間,在這個事件之前 duration 都是 NaN |
| 3 | loadedmetadata | 所有 metadata 讀取完畢 |
| 4 | loadeddata | 收到音檔的第一個 bit 時觸發,但還沒準備好播放 |
| 5 | progress | 下載音檔中 |
| 6 | canplay | 下載的音檔資料量足以開始播放時觸發,仍尚未完全載完 |
| 7 | canplaythrough | 下載整個音檔完成 |
Loading 過程中,則可能會被這些事件打斷而中止
| event | description |
|---|---|
| suspend | 暫停下載音源 |
| abort | 終止下載音源 |
| error | 錯誤 |
| emptied | 發生錯誤 or load() |
| stalled | 音源非預期的無法被取用 |
這些則是播放過程中會觸發的事件,搭配前面的 Method & Property 與監聽 Playing 事件,可以幫助我們處理各種 UI 呈現與互動功能,像是音量控制、時間軸拖移、播放/暫停/播放中斷/停止 ... 等。
| event | description |
|---|---|
| timeupdate | currentTime 屬性改變時觸發,每 250 ms 觸發一次,常用於進度條呈現 |
| waiting | 資料不足以繼續播放時觸發 |
| playing | waiting 後取得資料足以繼續播放時觸發 |
| play | play() 或 autoplay 發生時觸發 |
| pause | pause() 發生時觸發 |
| ended | 完全播放完畢時觸發 |
| volumechange | 音量改變時觸發,包含調整 muted 屬性 |
更詳細的 Media Event,可以參考這份Media Event 列表。
如果想知道播放器常見的互動,分別會觸發哪些事件的話,可以玩玩看這個 Media Event Inspector。
前面介紹完了 HTMLMediaElement ,看著看著發現竟然又有 HTMLAudioElement ,這兩者的差別是什麼呢?事實上,影音本來就不是能完全分割的東西,因此設計規範與瀏覽器實作時, HTMLMediaElement 是 HTMLAudioElement 和 HTMLVideoElement 的父類別,他們的屬性、方法、事件絕大多數都相同,只有少部分相異。像是 <audio> 不支援字幕 WebVTT 的播放,但可以用 <video> 單純播放聲音與字幕一樣。
其實 <audio> 就是 HTMLAudioElement 的實現,而 HTMLMediaElement 和 HTMLAudioElement 幾乎長一樣,唯一的差別是在「指定載入音源」的方法。還記得前面有提到,HTMLMediaElement 只能用 src 、 <source> 指定音源 URI,而 HTMLAudioElement 可以多在 constructor 帶入音源 URI ,長得像下面這樣:
const audioPlayer = new Audio('audiofile.mp3');
audioPlayer.play();
如果需要動態改音源 URI ,修改 HTMLAudioElement 的 src 屬性即可。
const audioPlayer = new Audio('audiofile.mp3');
audioPlayer.play();
audioPlayer.setAttribute('src', 'audiofile2.mp3');
audioPlayer.load();
audioPlayer.play();
今天就先到這邊,明天來正式實作播放器的功能吧!