最近在对超大音频的渐进式请求实现上面消耗了很多时间,主要是由于一对音频的基本原理不太理解,二刚开始的时候太依赖插件,三网上这块的资料找不到只能靠本身摸索。因为交互复杂加上坑比较多,我怕描述不清,这里主要根据问题来作描述(前提你须要对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; }); }
具体的代码含义我就不解释了,好累啊主要是涉及到不少位置的计算。不过好在最后完美实现啦~