基於wavesurfer.js的超大音訊的漸進式請求實現
最近在對超大音訊的漸進式請求實現上面消耗了不少時間,主要是因為一對音訊的基本原理不太理解,二剛開始的時候太依賴外掛,三網上這塊的資料找不到只能靠自己摸索。由於互動複雜加上坑比較多,我怕描述不清,這裡主要根據問題來做描述(前提你需要對wavesurfer.js有一定的瞭解)我的這篇部落格有做說明:Wavesurfer.js音訊播放器外掛的使用教程
實現效果:
未載入部分:
後端介面描述:
a、音訊主要資訊介面:獲取總時長、位元組數、總位元組、音訊格式等。
b、 分段請求介面:根據位元組引數,傳來對應段的音訊。
1、如何設定容器的長度,及滾動條的設定?
html佈局
<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;
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;
});
}
具體的程式碼含義我就不解釋了,好累啊主要是涉及到很多位置的計算。不過好在最後完美實現啦~