基於wavesurfer.js的超大音頻的漸進式請求實現

最近在對超大音頻的漸進式請求實現上面消耗了很多時間,主要是由於一對音頻的基本原理不太理解,二剛開始的時候太依賴插件,三網上這塊的資料找不到只能靠本身摸索。因爲交互複雜加上坑比較多,我怕描述不清,這裏主要根據問題來作描述(前提你須要對wavesurfer.js有必定的瞭解)個人這篇博客有作說明:Wavesurfer.js音頻播放器插件的使用教程
實現效果:
html

未加載部分:
web

後端接口描述:

a、音頻主要信息接口:獲取總時長、字節數、總字節、音頻格式等。
canvas

b、 分段請求接口:根據字節參數,傳來對應段的音頻。後端

一、如何設置容器的長度,及滾動條的設置?

html佈局api

<div class="wave-wrapper"  ref="waveWrapper" >
      <div
        class="wave-container"
        :style="{width: waveContainerWidth=='100%'?'100%':waveContainerWidth+'px'}"
        @click="containerClick($event)"
      >
        <div
          id="waveform"
          ref="waveform"
          class="wave-form"
          :style="{width: waveFormWidth=='100%'?'100%':waveFormWidth+'px',left: waveFormLeft+'px'}"
          @click.stop
        ></div>
      </div>
    </div>

waveform是wavesurfer渲染實際分段音頻的容器,waveWrapper是音頻的容器,這裏追溯wavesurfer的源碼,能夠知道它對音頻的解析是 **像素值=秒數*20**;所以從後端獲取總時長後,設置waveContainerWidth便可。樣式設置爲overflow-y:auto緩存

// 音頻寬:防止音頻太短,渲染不完
let dWidth = Math.round(that.duration * 20);
that.waveContainerWidth = that.wrapperWidth > dWidth ? that.wrapperWidth : dWidth;

二、音頻分幾段請求?

// 後臺傳入的音頻信息存儲
that.audioInfo = res;

// 音頻時長
 that.duration = parseInt(res.duration / 1000000);

// 若是音頻的長度大於500s分段請求,每段100s
// 1分鐘的字節數[平均] = 比特率(bps) * 時長(s)  /  8
that.rangeBit =   that.duration > 500 ? (that.audioInfo.bitrate * that.rangeSecond) / 8 : that.audioInfo.size;

 // 總段數
that.segNumbers = Math.ceil(that.audioInfo.size / that.rangeBit);

三、如何請求音頻文件,如何實現預加載?

wavesurfer.js渲染音頻的方式之一是根據WebAudio來渲染的。因爲後端傳給個人文件是arraybuffer的格式,那麼這裏就須要使用WebAudio讀取和解析buffer文件的功能,這些wavesurfer.js內部已實現。只須要將文件傳給它就能夠了。這裏我採用了預加載功能,即每加載一段音頻就繪製當段的音頻,但同時請求並緩存下一段音頻。app

/**
     * 獲取音頻片斷
     * @param segNumber 加載第幾段
     * @param justCache 僅僅緩存 true 僅緩存不加載
     * @param initLoad 初始加載
     */
    getAudioSeg(segNumber, justCache, initLoad) {
      let that = this;
      let xhr = new XMLHttpRequest();
      let reqUrl = location.origin;
      xhr.open(
        "GET",
        `${reqUrl}/storage/api/audio/${this.audioInfo.code}`,
        true
      );
      xhr.responseType = "arraybuffer";

      let startBit = this.rangeBit * (segNumber - 1);
      let endBit = this.rangeBit * segNumber;
      xhr.setRequestHeader("Range", `bytes=${startBit}-${endBit}`);

      xhr.onload = function() {
        if (this.status === 206 || this.status === 304) {
          let type = xhr.getResponseHeader("Content-Type");
          let blob = new Blob([this.response], { type: type });

          // 轉換成URL並保存
          that.blobPools[segNumber] = {
            url: URL.createObjectURL(blob)
          };

          // 第一次加載第一段,並對播放器事件進行綁定
          if (initLoad) {
            that.wavesurfer.load(that.blobPools[segNumber].url);
            that.currentSeg = 1;
            // 音頻事件綁定
            that.wavesurferEvt();
          } else if (!justCache) {
            that.currentSeg = segNumber;
            that.wavesurfer.load(that.blobPools[segNumber].url);
          }

          // 滾動條的位置隨着加載的位置移動
          if (!justCache && that.segNumbers > 1) {
            that.setScrollPos(segNumber);
          }
        }
      };

      xhr.onerror = function() {
        that.$message.error("音頻加載失敗,請重試");
        that.progress = false;
      };
      xhr.send();
    }

四、須要繪製的音頻怎麼建立?

this.wavesurfer = WaveSurfer.create({
   container:  that.$refs.waveform,
   waveColor: "#368666", //波紋
   progressColor: "#6d9e8b",
   hideScrollbar: false,隱藏波紋的橫座標
   cursorColor: "#fff",
   height: 80,
   responsive: true,
   scrollParent: true,
   maxCanvasWidth: 50000 // canvas的最大值
})

五、位置問題(主要的坑都在這裏)

一、實際請求獲取的音頻文件大小跟預計的大小並非徹底符合的。好比我每次想請求100s的視頻,根據字節公式算出來字節了,可是實際獲取到的音頻多是98s也多是102s。對應段的音頻位置怎麼放?這裏是在緩存音頻文件的時候記錄了波紋的實際位置:在wavesurfer的ready方法中(主要代碼):ide

// 記錄當斷的位置
        let pools = that.blobPools;

        // 第一段
        if (currentSeg == 1) {
          pools[currentSeg].startPos = 0;
          pools[currentSeg].endPos = that.waveFormWidth;
          // 預加載第二段
          if (segNumbers > 1) {
            that.getAudioSeg(2, true);
          }
        } else if (currentSeg == that.segNumbers) {
          // 最後一段
          pools[currentSeg].startPos =
            that.waveContainerWidth - that.waveFormWidth;
          pools[currentSeg].endPos = that.waveContainerWidth;
          console.log(pools);
          that.setScrollPos();
        } else {
          // 其餘段
          that.getAudioSeg(currentSeg + 1, true);
          if (pools[currentSeg - 1] && pools[currentSeg - 1].endPos) {
            pools[currentSeg].startPos = pools[currentSeg - 1].endPos;
            pools[currentSeg].endPos =
              pools[currentSeg].startPos + that.waveFormWidth;
          }
        }

二、個人可見區域就那麼大,若是音頻繪製的波形大於可見區域,如何在播放的時候自動設置滾動條的位置,把播放的區域顯示出來;這裏就要在wavesurfer的audioprocess方法中作處理(主要代碼):佈局

// 表示的是前面實際播放的
          let leftTime = that.waveFormScroll
            ? parseFloat(that.waveFormScroll) / 20
            : 0;

          // 當前實際的時間
          that.currentTime = parseInt(res + leftTime);

          // wave移動的距離
          let moveDis = Math.round(res * 20);

          // 滾動條的實際位置
          let scrollLeft = that.$refs.waveWrapper.scrollLeft;
          let waveFormLeft = that.waveFormLeft;
          let waveFormWidth = that.waveFormWidth; //wave
          let wrapperWidth = that.wrapperWidth;

          // 第一段的時候 moveDis - scrollLeft;
          // 第二段 waveFormLeft-scrollLeft+moveDis
          let actualDis;
          if (waveFormLeft == 0) {
            actualDis = moveDis - scrollLeft;
          } else {
            actualDis = waveFormLeft - scrollLeft + moveDis;
          }

          // 大於位置
          if (actualDis === wrapperWidth) {
            let dis =
              moveDis >= wrapperWidth
                ? waveFormWidth - moveDis
                : wrapperWidth - moveDis;
            that.$refs.waveWrapper.scrollLeft = scrollLeft + dis;
          }

三、加載對應段的時候,如何把渲染出來的波紋放在可視區域?這裏寫了個公用方法this

/**
     * 根據段設置容器的位置,保證波紋在可見區域
     * @param segNumber 請求段
     */
    setScrollPos(segNumber) {
      let n = segNumber ? segNumber : this.currentSeg;
      let segNumbers = this.segNumbers;
      let end = this.blobPools[n - 1] && this.blobPools[n - 1].endPos;
      // 最後一段,這裏是一個hack,爲了防止偏差
      if (n === segNumbers && this.blobPools[n] && this.blobPools[n].startPos) {
        end = this.blobPools[n].startPos;
      }

      this.waveFormScroll = end ? end : (n - 1) * this.wrapperWidth;
      this.waveFormLeft = this.waveFormScroll;
      this.$refs.waveWrapper.scrollLeft = this.waveFormScroll;
    }

四、當鼠標隨機點擊未加載音頻的位置時,如何保持加載的波紋位置並將波紋的位置進行移動,保證波紋加載後鼠標還在點擊的位置上?

/**
     * 隨機點擊容器
     * @param e 點擊的容器e
     */
    containerClick(e) {
      if (this.segNumbers == 1 || this.progress) {
        return;
      }
      // 點擊的位置記錄
      let layerX = e.layerX;

      // 記錄當前鼠標點擊的絕對位置
      let scrollLeft = this.$refs.waveWrapper.scrollLeft;
      this.clickWrapperPos = layerX - scrollLeft;

      // 獲取點擊的時間點
      let currentTime = parseInt(layerX / 20);

      // 獲取字節所在
      let { size, duration, bitrate } = this.audioInfo;
      let currentBit = (bitrate * currentTime) / 8;
      let seg = Math.ceil(currentBit / this.rangeBit);

      // 由於音樂的動態性,因此請求的段數會存在偏差,這個時候更改請求的段數
      if (seg == this.currentSeg) {
        // let currentMinTime = 60 * (this.currentSeg-1);
        // let currentMaxTime = 60 * this.currentSeg;
        let average = (120 * this.currentSeg - this.rangeSecond) / 2;
        seg = currentTime > average ? seg + 1 : seg - 1;
      }
       this.currentTime = currentTime;

        // 有緩存數據
        this.progress = true;
        if (this.blobPools[seg]) {
          // 加載緩存數據
          this.wavesurfer.load(this.blobPools[seg].url);

          // 更改當前的播放段數
          this.currentSeg = seg;
          this.setScrollPos();
        } else {
          this.getAudioSeg(seg);
        }
        // 記錄這是點擊請求的波紋,在波紋的ready方法中作處理
        this.fromSeek = true;
      }
    }

ready方法中加入處理:

// 點擊來的
        if (that.fromSeek) {
          let leftTime = parseFloat(that.waveFormScroll) / 20;
          let moveTime = Math.abs(that.currentTime - leftTime);
          that.wavesurfer.skip(moveTime);

          // 指針的位置移動到當時指的clickWrapperPos位置上,體驗更好,這裏不能改變波紋的位置,須要改變滾動條的位置
          that.$nextTick(() => {
            let movePos = moveTime * 20;
            let disPos = that.clickWrapperPos - movePos;
            // 左-
            // 右+
            let scrollLeft = that.$refs.waveWrapper.scrollLeft;
            if (disPos > 0) {
              that.$refs.waveWrapper.scrollLeft = scrollLeft - disPos;
            } else {
              that.$refs.waveWrapper.scrollLeft = scrollLeft + Math.abs(disPos);
            }
            that.fromSeek = false;
            that.clickWrapperPos = 0;
          });
        }

具體的代碼含義我就不解釋了,好累啊主要是涉及到不少位置的計算。不過好在最後完美實現啦~

相關文章
相關標籤/搜索