音頻組件開發實踐

個人blog 原文連接

最近公司迭代的項目中,新增了音頻播放功能,填了很多音頻的坑,總結一下:vue

需求:

交互需求:ios

  1. 上傳:點擊按鈕上傳語音,返回文件id(上傳文件的範疇,本文不會闡述)
  2. 播放:點擊播放按鈕,異步獲取語音播放src,音頻載入以後播放
  3. 操做:語音支持播放、暫停、中止、進度拖動等操做

固然還有一些隱性需求:git

  1. 一個界面可能存在多個播放文件
  2. 隨時播放一個語音,其它語音應當暫停
  3. 播放過程當中,用戶從新上傳新的語音,此時播放應中止

實現效果以下圖所示:(固然,這只是項目用到的一部分,項目中還有其餘頁面也用到了這個組件,那麼就更考驗組件的健壯性和可拓展性了。)github

音頻播放效果

按需實現

一個界面可能存在多個播放文件

咱們對音頻的操做,一般是先獲取這個音頻 DOM Element,經過對它的操做,實現想要達到的效果,若是你只是設定一個audio這樣單薄的ref名稱,恐怕會有些問題,所以我給每一個音頻設定了一個惟一的ref名稱。chrome

<template>
  <audio :src="audioSrc" :ref='uniqueId' :data-key="uniqueId" hidden></audio>
</template>
<script>
export default {
  data () {
    return {
      // uniqueId() 是隨機生成字符串的方法
      uniqueId: uniqueId(),
      audioSrc: '',
    }
  },
  computed: {
    audioElement () {
      return this.$refs[this.uniqueId]
    }
  },
}
</script>

暫停其餘語音

注意到上面的代碼,我在給 audio 添加屬性的時候,多添加了一個 data-key的屬性,那就是爲了暫停其餘語音而使用的,做爲我要操做頁面其餘音頻而設置的標識:segmentfault

// 暫停其餘語音的方法
pauseOthers (except) {
  var audios = document.querySelectorAll('audio')
  ;[].forEach.call(audios, audio => {
    if (audio.dataset['key'] !== except.uniqueId) {
      audio.pause()
    }
  })
}
// 調用的時候
this.pauseOthers (this)

異步獲取語音src,音頻載入以後播放

我想這就是項目坑點之一,由於音頻src並非上傳語音就返回的,上傳語音只返回了語音id,咱們須要經過id再去異步請求一次,才能獲取到src。後端

基於這樣的前提,播放操做作了兩點考慮(單例模式思惟):promise

  • 爲何點擊播放再獲取語音src?雖然也能夠進入界面就請求src,可是若是用戶不點擊播放,就白白浪費了不須要的請求,基於性能的考慮,決定點擊播放後再進行請求。
  • 並不須要每次點擊都從新請求一次,只有未獲取過src的音頻須要從新請求。

具體實現:瀏覽器

  1. 播放按鈕綁定togglePlay()事件
  2. 判斷audioSrc是否有值bash

    • 若是有值,直接進行播放,綁定相關事件,暫停其餘語音
    • 若是沒有值,設置loading並進行異步請求,將返回結果賦值給audioSrc,監聽音頻 canplay
  3. 監聽音頻 canplay (這邊有一個坑點,後面會提到)
    在canplay的回調中,loading結束,綁定相關事件,暫停其餘語音

爲何相關事件的綁定放在 canplay 中? 否則你可能會出現下面的報錯:

Uncaught (in promise) DOMException: The element has no supported sources.

因此,答應我,基於audio播放的 事件 或是 屬性 ,都放在 canplay 的回調以後。

相關事件包括(本組件中):

  1. 監聽事件 timeupdate : 控制進度條展現
  2. 監聽事件 pause : 監聽按鈕 播放/暫停 樣式
  3. 設置屬性 currentTime : 控制進度拖動或者中止語音
  4. 監聽事件 error : 監聽播放錯誤

音頻的操做

播放與暫停

按鈕的樣式經過設置一個變量做爲狀態值,paly()pause() 的時候分別改變狀態值。

其它具體邏輯上文描述比較清楚,再也不贅述。

中止、進度拖動

  • 中止:暫停音頻,並將currentTime設置爲0
  • 進度拖動:根據拖動位置計算currentTime值,並設置currentTime

兩個操做都涉及到了currentTime的設置,咱們在這裏遇到了兩個坑:

  1. 設置currentTime無效
    查詢資料後發現是後端的鍋,具體解決辦法連接在這裏:HTML5 audio ,在chrome中設置currentTime無效
  2. 設置currentTime繼續播放

    一開始仍然覺得是後端的鍋,由於當我靜態設置一個 audioSrc 的時候,是沒有問題的,可是當我動態設置,就會出現這樣的問題:不管我是播放狀態仍是暫停狀態,設置到相對應的currentTime都會繼續播放。

    經過排查,發現當我設置currentTime會再次觸發一次 canplay事件, canplay 的回調是綁定播放的相關操做,所以會繼續播放。

    解決辦法,溫習了一遍addEventListener的語法,綁定canplay事件最多隻調用一次。

    this.audioElement.addEventListener('canplay', () => {
          // ...相關操做
    }, {
      once: true
    })

音頻的打斷

音頻的打斷包括兩種狀況:

  1. 組件 destroyed
  2. 從新上傳新的語音

第一種狀況,解綁相關事件,釋放內存。

第二種狀況,具體描述一下:

當用戶從新上傳新的語音,不論此時語音暫停仍是播放狀態,都應該中止。

咱們經過 watch 監聽 id (上傳返回來的音頻id),當id變化的時候,將 audioSrc 清空,以避免播放舊的音頻內容。

然而,僅僅這樣是不夠的,若是監聽 error 事件,就會發現報錯,解決的辦法仍是解綁相關事件,即,咱們在 canplay 回調中的綁定的相關事件,讓audio恢復初始狀態,等到下一次播放的時候,須要從新請求新的src,回到上面播放的部分。

拓展

在解決問題的過程當中,也查詢到了一些實用的知識點,雖然在應用中沒有運用到,可是這些知識點看起來彷佛挺有用的,爲了下次遇到其餘坑能快速找到解決辦法,先把這些知識點記錄下來。

canplaycanplaythrough 辨析

  • 當瀏覽器可以開始播放指定的音頻/視頻時,會發生 canplay 事件。
  • 當瀏覽器預計可以在不停下來進行緩衝的狀況下持續播放指定的音頻/視頻時,會發生 canplaythrough 事件。
  • 瞭解其餘媒體相關事件

HTMLMediaElement.play() 返回 Promise

<video><audio>play()返回一個 Promise,若是播放成功,Promise狀態變成fulfilled,若是播放失敗,狀態變爲rejected並提供錯誤信息。

var playPromise = document.querySelector('video').play();

// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
  playPromise.then(function() {
    // Automatic playback started!
  }).catch(function(error) {
    // Automatic playback failed.
    // Show a UI element to let the user manually start playback.
  });
}

video 412錯誤

412 通常是由於服務器的 If-Unmodified-Since 或 If-None-Match 未實現
// 解決辦法
media.addEventListener('error', function (e) {
   var date = new Date();
   var milliSecs = date.getMilliseconds();
   var curr_src = $(media[0]).attr('src');
   var curr_src_arr = curr_src.split("?");
   var new_src = curr_src_arr[0]+"?t="+milliSecs;

   $(media[0]).attr('src',new_src);
   $(media[0]).find('source').attr('src',new_src);
   media[0].load();
}, false);

暫時完。

後續若是測試妹妹發現了什麼bug,我會繼續填坑記錄滴。

相關文章
相關標籤/搜索