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

未加载部分:

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

b、 分段请求接口:根据字节参数,传来对应段的音频。
1、如何设置容器的长度,及滚动条的设置?
html布局
waveform是wavesurfer渲染实际分段音频的容器,waveWrapper是音频的容器,这里追溯wavesurfer的源码,可以知道它对音频的解析是 **像素值=秒数*20**;因此从后端获取总时长后,设置waveContainerWidth即可。样式设置为overflow-y:auto。
// 音频宽:防止音频过短,渲染不完letdWidth =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) / 8that.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) {letthat =this;letxhr =newXMLHttpRequest();letreqUrl = location.origin; xhr.open("GET",`${reqUrl}/storage/api/audio/${this.audioInfo.code}`,true); xhr.responseType ="arraybuffer";letstartBit =this.rangeBit * (segNumber -1);letendBit =this.rangeBit * segNumber; xhr.setRequestHeader("Range",`bytes=${startBit}-${endBit}`); xhr.onload =function(){if(this.status ===206||this.status ===304) {lettype = xhr.getResponseHeader("Content-Type");letblob =newBlob([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(); }elseif(!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方法中(主要代码):
// 记录当断的位置letpools = that.blobPools;// 第一段if(currentSeg ==1) { pools[currentSeg].startPos =0; pools[currentSeg].endPos = that.waveFormWidth;// 预加载第二段if(segNumbers >1) { that.getAudioSeg(2,true); } }elseif(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方法中做处理(主要代码):
// 表示的是前面实际播放的letleftTime = that.waveFormScroll ?parseFloat(that.waveFormScroll) /20:0;// 当前实际的时间that.currentTime =parseInt(res + leftTime);// wave移动的距离letmoveDis =Math.round(res *20);// 滚动条的实际位置letscrollLeft = that.$refs.waveWrapper.scrollLeft;letwaveFormLeft = that.waveFormLeft;letwaveFormWidth = that.waveFormWidth;//waveletwrapperWidth = that.wrapperWidth;// 第一段的时候 moveDis - scrollLeft;// 第二段 waveFormLeft-scrollLeft+moveDisletactualDis;if(waveFormLeft ==0) { actualDis = moveDis - scrollLeft; }else{ actualDis = waveFormLeft - scrollLeft + moveDis; }// 大于位置if(actualDis === wrapperWidth) {letdis = moveDis >= wrapperWidth ? waveFormWidth - moveDis : wrapperWidth - moveDis; that.$refs.waveWrapper.scrollLeft = scrollLeft + dis; }
3、加载对应段的时候,如何把渲染出来的波纹放在可视区域?这里写了个公用方法
/**
* 根据段设置容器的位置,保证波纹在可见区域
* @param segNumber 请求段
*/setScrollPos(segNumber) {letn = segNumber ? segNumber :this.currentSeg;letsegNumbers =this.segNumbers;letend =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; }// 点击的位置记录letlayerX = e.layerX;// 记录当前鼠标点击的绝对位置letscrollLeft =this.$refs.waveWrapper.scrollLeft;this.clickWrapperPos = layerX - scrollLeft;// 获取点击的时间点letcurrentTime =parseInt(layerX /20);// 获取字节所在let{ size, duration, bitrate } =this.audioInfo;letcurrentBit = (bitrate * currentTime) /8;letseg =Math.ceil(currentBit /this.rangeBit);// 因为音乐的动态性,所以请求的段数会存在误差,这个时候更改请求的段数if(seg ==this.currentSeg) {// let currentMinTime = 60 * (this.currentSeg-1);// let currentMaxTime = 60 * this.currentSeg;letaverage = (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) {letleftTime =parseFloat(that.waveFormScroll) /20;letmoveTime =Math.abs(that.currentTime - leftTime); that.wavesurfer.skip(moveTime);// 指针的位置移动到当时指的clickWrapperPos位置上,体验更好,这里不能改变波纹的位置,需要改变滚动条的位置that.$nextTick(()=>{letmovePos = moveTime *20;letdisPos = that.clickWrapperPos - movePos;// 左-// 右+letscrollLeft = 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; }); }