zoukankan      html  css  js  c++  java
  • 基于wavesurfer.js的超大音频的渐进式请求实现

    最近在对超大音频的渐进式请求实现上面消耗了不少时间,主要是因为一对音频的基本原理不太理解,二刚开始的时候太依赖插件,三网上这块的资料找不到只能靠自己摸索。由于交互复杂加上坑比较多,我怕描述不清,这里主要根据问题来做描述(前提你需要对wavesurfer.js有一定的了解)我的这篇博客有做说明:Wavesurfer.js音频播放器插件的使用教程
    实现效果:

    未加载部分:

    后端接口描述:

    a、音频主要信息接口:获取总时长、字节数、总字节、音频格式等。

    b、 分段请求接口:根据字节参数,传来对应段的音频。

    1、如何设置容器的长度,及滚动条的设置?

    html布局

    <div class="wave-wrapper"  ref="waveWrapper" >
          <div
            class="wave-container"
            :style="{ waveContainerWidth=='100%'?'100%':waveContainerWidth+'px'}"
            @click="containerClick($event)"
          >
            <div
              id="waveform"
              ref="waveform"
              class="wave-form"
              :style="{ 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;               
    

    2、音频分几段请求?

    // 后台传入的音频信息存储
    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);
    

    3、如何请求音频文件,如何实现预加载?

    wavesurfer.js渲染音频的方式之一是根据WebAudio来渲染的。由于后端传给我的文件是arraybuffer的格式,那么这里就需要使用WebAudio读取和解析buffer文件的功能,这些wavesurfer.js内部已实现。只需要将文件传给它就可以了。这里我采用了预加载功能,即每加载一段音频就绘制当段的音频,但同时请求并缓存下一段音频。

        /**
         * 获取音频片段
         * @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();
        } 
    

    4、需要绘制的音频怎么创建?

    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的最大值
    })                    
    

    5、位置问题(主要的坑都在这里)

    1、实际请求获取的音频文件大小跟预计的大小并不是完全符合的。比如我每次想请求100s的视频,根据字节公式算出来字节了,但是实际获取到的音频可能是98s也可能是102s。对应段的音频位置怎么放?这里是在缓存音频文件的时候记录了波纹的实际位置:在wavesurfer的ready方法中(主要代码):

            // 记录当断的位置
            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;
              }
            }
    

    2、我的可见区域就那么大,如果音频绘制的波形大于可见区域,如何在播放的时候自动设置滚动条的位置,把播放的区域显示出来;这里就要在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;
              }
    

    3、加载对应段的时候,如何把渲染出来的波纹放在可视区域?这里写了个公用方法

        /**
         * 根据段设置容器的位置,保证波纹在可见区域
         * @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;
        }
    

    4、当鼠标随机点击未加载音频的位置时,如何保持加载的波纹位置并将波纹的位置进行移动,保证波纹加载后鼠标还在点击的位置上?

        /**
         * 随机点击容器
         * @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;
              });
            }
    

    具体的代码含义我就不解释了,好累啊主要是涉及到很多位置的计算。不过好在最后完美实现啦~

  • 相关阅读:
    spring框架里面处理中文匹配
    日常问题记录--使用fiddler自动响应jsonp结构的响应
    linux命令--pamp
    每天一个linux命令--nice命令
    阿里RAP+fiddler实现app原生应用的cgi数据mock----- (二)添加mock规则,随机返回4中类型(不同长度)的数据
    父子组件之间传递数据
    redux-API(二)
    redux数据流
    Redux 的基础概念-API
    react-redux要点梳理
  • 原文地址:https://www.cnblogs.com/webhmy/p/10175785.html
Copyright © 2011-2022 走看看