H5 Audio ES6版 系列教程之二

EER發表於2019-02-26

瀏覽器支援情況

為保證功能的正常,基本的瀏覽器檢測是必要的。

  • 瀏覽器是否支援 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 中的遇到的坑來講解。

謝謝閱讀!

其他分享

以下推薦閱讀,讀者可選讀:

相關文章