HTML5中Audio使用踩坑彙總

三隻小羊發表於2019-03-01
  • Cannot read property `catch` of undefined

    原因:在呼叫play()時,現代瀏覽器返回的是一個promise,對於執行失敗的,會觸發一個Unhandled Promise Rejection,但是對於低版本的瀏覽器,呼叫play()並不會返回一個promise。

    解決:應該在呼叫play()時做如下處理,增加對playPromise的判斷

    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.
      });
    }
    複製程式碼

    參考資料:HTMLMediaElement.play() Returns a Promise

  • InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable

    原因:對於還沒有設定src的audio,就直接設定currentTime是會觸發一個INVALID_STATE_ERR異常的。即使是設定currentTime = 0也會觸發這個異常

    解決:在設定currentTime之前,必須先設定audio的src

    參考資料:Offsets into the media resource

    media . currentTime [ = value ]

    Returns the current playback position, in seconds.

    Can be set, to seek to the given time.

    Will throw an INVALID_STATE_ERR exception if there is no selected media resource. Will throw an INDEX_SIZE_ERR exception if the given time is not within the ranges to which the user agent can seek.

  • NotAllowedError

    原因:在呼叫play()時可能會觸發一個NotAllowedError的reject,原因是因為瀏覽器在某些情況下播放失敗,常見場景是,未通過點選的情況下呼叫play() ,或者點選事件的回撥中是在下一個tick裡呼叫的play,例如在setTimeout裡呼叫的play,再或者新建立了很多個audio元素,但是並不是每個audio都是通過使用者點選來呼叫的play()等等。

    • 場景一:未通過點選等事件繫結,直接呼叫play(),觸發NotAllowedError。解決方法,把呼叫play()的部分放在事件回撥裡,如下程式碼:

      playButton.addEventListener("click", () => {
          audioElem.play()
      }, false);
      複製程式碼
    • 場景二:在點選事件回撥中的下一個tick裡呼叫play(),這種情況的示例程式碼如下,

      // 錯誤程式碼示例
      playButton.addEventListener("click", () => {
          setTimeout(() => {
              audioElem.play()
          }, 100)
      }, false);
      複製程式碼

      這種情況,某些版本【在iOS12.0.1親測有坑】也會觸發NotAllowedError異常,應該避免這種情況,可以考慮如下hack手段解決

      // hack 
      playButton.addEventListener("click", () => {
          audioElem.muted = true
          let p = audioElem.play()
          if (p !== undefined) {
              p.then(() => {
                  audioElem.muted = false
                  audioElem.pause()
                  setTimeout(() => {
                      audioElem.play()
                  }, 100)
              }).catch((e) => {
                  console.log(e)
              })
          }
      }, false);
      複製程式碼
    • 場景三:建立了多個audio元素,但是並不是每個audio都是通過使用者點選來呼叫的play()的,這時候某些版本【在iOS12.0.1親測有坑】也會觸發NotAllowedError異常。對於這種情況,最好的辦法就是隻建立一個audio元素,後面通過改變src來播放不同的音樂資源。只要audio通過了事件回撥裡呼叫過play,後續都可以直接呼叫play了,而無需再次繫結事件回撥裡去執行,並且這樣也可以避免建立多個audio來減少記憶體使用。

      playButton.addEventListener("click", () => {
          audioElem.src = "https://a.mp3"
          audioElem.play()
      }, false);
      
      // 後面其他地方,可以改變src來直接play
      audioElem.src = "https://b.mp3"
      audioElem.play()
      複製程式碼
  • iOS 中頁面隱藏和顯示時,播放audio行為異常

    原因:在某些iOS版本中【iOS12.0.1親測有坑】,當我們監聽頁面隱藏和顯示事件,在隱藏時呼叫pause() 暫停,顯示時呼叫play()恢復播放。當按下home鍵,頁面進入系統後臺時,pause()正常呼叫,audio被正常暫停,但是但再次進入頁面,顯示事件中呼叫play()就會出現異常了,

    • 第一種異常,如果,我們只是單純的呼叫audioElem.play(),不會丟擲任何錯誤,但是audio實際卻沒有真正播放,無任何聲音;

    • 第二種異常,如果我們每次在顯示事件中執行如下程式碼中任意一種場景,都會在很多情況下會丟擲一個AbortError異常,極少數情況才會正常播放。

      //監聽頁面顯示隱藏事件
      addPageVisibilityListener(() => {
          // 隱藏時暫停
        audioElem.pause()
      },() => {
          // 顯示時恢復播放
          
          // 重新直接賦值src
          audioElem.src = "https://b.mp3"
          audioElem.play()
          
          // 或者load
          // audioElem.load()
          // audioElem.play()
      })
      複製程式碼

    解決:這兩種異常行為應該都是iOS 12.0.1系統本身的bug。我們可以通過如下2中方式來避免這種兩種異常的發生,

    • 方式1, 顯示load(),並監聽canplaythrough,推薦使用這種方式
    const playAudio = () => {
      audioElem1.removeEventListener(`canplaythrough`, playAudio)
      let p = audioElem.play()
      if (p !== undefined) {
        p.catch((e) => {
          console.log(e)
        })
      }
    }
    //監聽頁面顯示隱藏事件
    addPageVisibilityListener(() => {
        // 隱藏時暫停
      audioElem.pause()
    },() => {
        // 顯示時恢復播放
        audioElem.load()
        audioElem.addEventListener(`canplaythrough`, playAudio)
    })
    複製程式碼
    • 方式2,通過setTimeout以及retry來hack避免這種異常發生
    let playAudio = (retry: boolean) => {
        let p = audioElem.play();
        if (p !== undefined) {
            p.catch((e) => {
                if (retry) {
                    setTimeout(() => {
                        playAudio(false);
                    }, 0);
                }
            });
        }
    }
    //監聽頁面顯示隱藏事件
    addPageVisibilityListener(() => {
        // 隱藏時暫停
      audioElem.pause()
    },() => {
        // 顯示時恢復播放
        setTimeout(() => {
            playAudio(true)
        }, 500)
    })
    複製程式碼

相關文章