爲保證功能的正常,基本的瀏覽器檢測是必要的。javascript
另外兼容性這塊推薦 Modernizrhtml
使用數組來存儲 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 操做,可基於原生querySelectorAll查詢播放器相關 DOM 並暫存起來,(如播放按鈕,進度條,聲音控制器等)。vue
控制器相關的事件有java
input
,故監聽 Change
來更新進度條 )播放和音量控制都涉及兩種狀態,可使用 toggle 的方式來控制。webpack
togglePlay(toggle) {
if (!_.is.boolean(toggle)) {
toggle = this.media.paused;
}
if (toggle) {
this.play();
} else {
this.pause();
}
return toggle;
}
複製代碼
進度條控制中 input change 觸發的 seek 回調git
//
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');
}
複製代碼
同播放控制github
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
觸發時,獲取時長 或者 手動設置時長。web
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);
}
複製代碼
// 進度條有兩種,一、已播放的 二、緩存
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
);
}
複製代碼
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 中的遇到的坑來說解。
謝謝閱讀!
如下推薦閱讀,讀者可選讀: