瀏覽器支援情況
為保證功能的正常,基本的瀏覽器檢測是必要的。
- 瀏覽器是否支援 Audio 標籤
- Audio 內建方法和屬性在不同瀏覽器下表現不一致或未實現等情況,此時應該區分瀏覽器型別及版本。
另外相容性這塊推薦 Modernizr
UI構建
- 生成播放器的 DOM 結構,並插入到文件樹中。
使用陣列來儲存 DOM 結構,通過配置項選擇性地往陣列裡 push
相應的 DOM,最終使用 join
匯出 DOM 的字串格式。通過 insertAdjacentHTML把 DOM 插入文件樹中。為方便以後 DOM 的操作和整體狀態樣式的控制,可以將控制器 DOM 結構跟 Audio 標籤包裹在同一 DIV 中。
html.push(`<div class="myaudio__controls">`);
if (_.inArray(controls, `play`)) {
html.push(
`
<button type="button" data-myaudio="play">
<svg><use xlink:href="#${icon.play}" /></svg>
</button>
<button type="button" data-myaudio="pause">
<svg><use xlink:href="#${icon.pause}" /></svg>
</button>
`
);
}
html.push(`</div>`);
html.join(``);
複製程式碼
- 儲存 DOM 引用
為方便 DOM 操作,可基於原生querySelectorAll查詢播放器相關 DOM 並暫存起來,(如播放按鈕,進度條,聲音控制器等)。
控制器
控制器相關的事件有
- 播放按鈕點選事件
- 進度條 seek 事件( 由於使用的是
input
,故監聽Change
來更新進度條 ) - 靜音控制按鈕的點選事件
- 音量控制的原理跟進度條類似
播放控制
播放和音量控制都涉及兩種狀態,可以使用 toggle 的方式來控制。
togglePlay(toggle) {
if (!_.is.boolean(toggle)) {
toggle = this.media.paused;
}
if (toggle) {
this.play();
} else {
this.pause();
}
return toggle;
}
複製程式碼
進度條控制
進度條控制中 input change 觸發的 seek 回撥
//
seek(input) {
// 跳轉時間
let targetTime = 0;
let paused = this.media.paused;
// 獲取總時長
let duration = this.getDuration();
if (_.is.number(input)) {
// 如果傳入的引數為數值則直接設定為目標時間
targetTime = input;
} else if (
// 一般情況下,傳入的引數為 input 元素,
// 由於 targetTime / duration = input.target.value / input.target.max
// 所以跳轉時間為 targetTime = input.target.value / input.target.max * duration;
_.is.object(input) &&
_.inArray([`input`, `change`], input.type)
) {
targetTime = input.target.value / input.target.max * duration;
}
// 特殊情況處理
// 讓跳轉時間的區間在 0 至 總時長
if (targetTime < 0) {
targetTime = 0;
} else if (targetTime > duration) {
targetTime = duration;
}
// 更新進度條
this.updateSeekDisplay(targetTime);
// 讓Audio元素的當前時間等於跳轉時間完成跳轉
// TODO:如果需求是拖動進度條過程不改變音訊當前時間則需要做其他處理
try {
this.media.currentTime = targetTime.toFixed(4);
} catch (e) {}
// 確保音訊暫停狀態一致
if (paused) {
this.pause();
}
// 觸發 audio 原生 timeupdate 和 seeking,目的是狀態保持一致
// 這裡涉及 自定義事件 想深入的同學可自行了解
this.triggerEvent(this.media, `timeupdate`);
this.triggerEvent(this.media, `seeking`);
}
複製程式碼
靜音控制
同播放控制
音量設定
setVolume(volume) {
let max = this.config.volumeMax;
let min = this.config.volumeMin;
// 取storage的值
if (_.is.undefined(volume)) {
volume = this.storage.volume;
}
// 取預設值
if (volume === null || isNaN(volume)) {
volume = this.config.volume;
}
// 控制音量區間在 min 至 max
if (volume > max) {
volume = max;
}
if (volume < min) {
volume = min;
}
this.media.volume = parseFloat(volume / max);
// 同步 音量的進度條
if (this.volume.display) {
this.volume.display.value = volume;
}
// 音量確定是否靜音
if (volume === 0) {
this.media.muted = true;
} else if (this.media.muted && volume > 0) {
this.toggleMute();
}
}
複製程式碼
媒體事件
獲取時長
durationchange
loadedmetadata
觸發時,獲取時長 或者 手動設定時長。
displayDuration() {
// 支援ie9 以上
if (!this.supported.full) {
return;
}
// Audio 很多事件都基於 duration 正常獲取,但是duration在每個裝置中值可能不同
// TODO:當需要懶載入時,音訊不載入則無法獲取時長,此時需手動設定
let duration = this.getDuration() || 0;
// 只在開始的時候顯示時長,設定的條件是沒有時長的DOM,displayDuration 為true,視訊暫停時。
if (!this.duration && this.config.displayDuration && this.media.paused) {
this.updateTimeDisplay(duration, this.currentTime);
}
if (this.duration) {
// 轉換時間格式後,通過 innerHTML 直接設定
this.updateTimeDisplay(duration, this.duration);
}
}
複製程式碼
更新時間
timeupdate
seeking
觸發時,更新時間
timeUpdate(event) {
// 更新音訊當前時間
this.updateTimeDisplay(this.media.currentTime, this.currentTime);
if (event && event.type === `timeupdate` && this.media.seeking) {
return;
}
// 更新進度條
this.updateProgress(event);
}
複製程式碼
更新進度條
progress
playing
觸發時,更新快取時長
updateProgress(event) {
if (!this.supported.full) {
return;
}
let progress = this.progress.played;
let value = 0;
let duration = this.getDuration();
if (event) {
switch (event.type) {
// 已播放時長設定
case `timeupdate`:
case `seeking`:
value = this.getPercentage(this.media.currentTime, duration);
if (event.type === `timeupdate` && this.buttons.seek) {
this.buttons.seek.value = value;
}
break;
// 快取時長設定
case `playing`:
case `progress`:、
progress = this.progress.buffer;
value = (() => {
let buffered = this.media.buffered;
if (buffered && buffered.length) {
return this.getPercentage(buffered.end(0), duration);
} else if (_.is.number(buffered)) {
return buffered * 100;
}
return 0;
})();
break;
}
}
// Set values
this.setProgress(progress, value);
}
複製程式碼
// 進度條有兩種,1、已播放的 2、快取
setProgress(progress, value) {
if (!this.supported.full) {
return;
}
if (_.is.undefined(value)) {
value = 0;
}
if (_.is.undefined(progress)) {
if (this.progress && this.progress.buffer) {
progress = this.progress.buffer;
} else {
return;
}
}
if (_.is.htmlElement(progress)) {
progress.value = value;
} else if (progress) {
if (progress.bar) {
progress.bar.value = value;
}
if (progress.text) {
progress.text.innerHTML = value;
}
}
}
複製程式碼
音量控制
volumechange
觸發時,更新音量
updateVolume() {
// 靜音時,音量為0
let volume = this.media.muted
? 0
: this.media.volume * this.config.volumeMax;
if (this.supported.full) {
if (this.volume.input) {
// 音訊控制圓點位置更新
this.volume.input.value = volume;
}
if (this.volume.display) {
// 音量位置更新
this.volume.display.value = volume;
}
}
this.updateStorage({ volume: volume });
// 新增靜音全域性樣式控制類
_.toggleClass(this.container, this.config.classes.muted, volume === 0);
}
複製程式碼
播放控制
play
pause
ended
觸發時,更新播放狀態
checkPlaying() {
// 暫停和播放樣式切換
_.toggleClass(
this.container,
this.config.classes.playing,
!this.media.paused
);
_.toggleClass(
this.container,
this.config.classes.stopped,
this.media.paused
);
}
複製程式碼
Loading
waiting
canplay
seeked
觸發時,更新loading狀態
checkLoading(event) {
let loading = event.type === `waiting`;
let _this = this;
clearTimeout(this.timers.loading);
// 當不是 waiting 事件時,把事件在當前呼叫棧最後執行
this.timers.loading = setTimeout(function() {
_.toggleClass(_this.container, _this.config.classes.loading, loading);
}, loading ? 250 : 0);
}
複製程式碼
總結
audio 的事件並不多,DOM結構也並不複雜。只要大家按上面的思路理一遍,基本也能自己寫一個原生的audio。
實現方式大同小異,只是需求不同。希望這個教程對大家有所幫助。接下來的教程系列將會從audio 中的遇到的坑來講解。
謝謝閱讀!
其他分享
以下推薦閱讀,讀者可選讀: