聲明:文章内容僅供學習交流。
通過開發者工具檢視網站,不難發現,Polyv視訊是使用canvas标簽來渲染視訊畫面的,音頻是通過AudioContext進行播放的。
此外,網站的視訊是加密的,如果想要下載下傳視訊,比較好的辦法是找出視訊資料的解密算法,然後去解密恢複出視訊資料。但這個思路比較費時費力,一時半會很難有進展。是以這篇文章裡,我們先來嘗試一個實踐比較簡單的思路。因為視訊和音頻是分開的,是以我們依次來分析。
視訊
因為視訊是使用的是canvas标簽,而canvas繪制畫面的常見方法包括:drawImage(2d)、putImageData(2d)、drawArrays(webgl)。
搜尋圖檔繪制方法
1)首先全局搜尋這幾個方法(總共隻搜到4個),均打上斷點;
打斷點
2)在網頁中播放視訊,看哪個斷點會随着視訊的播放不斷被觸發;
通過調試發現,頻繁觸發的是drawArrays方法,在代碼的其它地方也可以發現webgl的字樣。視訊渲染之是以使用webgl,應該是因為它的性能要比2d快好幾倍。
3)将canvas繪制畫面時使用的圖檔資料下載下傳到本地;
在上一步中,既然已經開始使用drawArrays渲染畫面了,那麼此時的資料必然已經是解密後的資料,是以我們在該方法的地方插入代碼,将圖檔資料直接下載下傳到本地;
下載下傳圖檔
因為新增的代碼是同步操作,是以,在邊播放邊儲存的過程中,會導緻視訊播放不流暢,體驗不好;
觀察下載下傳後的圖檔,每張圖檔大概有1M多。
檢視圖檔
4)使用MediaRecorder來下載下傳視訊資料
因為上一步中的下載下傳方式體驗較差,是以我嘗試使用MediaRecorder來替代。在文檔中可以看到,MediaRecorder支援傳入來自canvas的資料流。
MediaRecorder文檔
我們通過建立MediaRecorder對象,并将頁面中的canvas元素作為資料源,對視訊進行錄制。相關代碼如下:
let videoRecorder = null;
let mediaStream = null;
let videoChunks = [];
let isRecordVideo = false;
let canvasObj = null;
// 初始化MediaRecorder & 綁定MediaRecorder事件
function initVideoRecorder() {
if (!mediaStream) return
// 音頻比特率128kbps,視訊比特率2.5Mbps
const options = {
audioBitsPerSecond : 128000,
videoBitsPerSecond : 500000,
mimeType : 'video/webm;codecs=h264'
};
videoRecorder = new MediaRecorder(mediaStream, options);
// 結束後擷取資料
videoRecorder.ondataavailable = (e) => {
// 記錄視訊片段
if (e.data && e.data.size) {
// this.videoChunks.push(e.data)
// 分片段輸出(适用于錄制時間較長檔案較大的情況)
this.videoChunks2DataUrl([e.data]).then((data) => {
console.log('fileReader onloadend:', data)
// 儲存視訊
downloadFileByA(data.target.result)
})
}
}
// 開始
videoRecorder.onstart = (e) => {
// 每次開始錄制時,清空chunks
videoChunks = [];
};
// 結束
videoRecorder.onstop = (e) => {
// 錄制結束後,處理chunks,生成檔案
let blob = new Blob(videoChunks, {type: 'video/webm'});
let fileReader = new FileReader();
fileReader.readAsDataURL(blob);
fileReader.onloadend = (data) => {
// 儲存視訊
downloadFileByA(data.target.result)
};
};
// 暫停
videoRecorder.onpause = (e) => {};
// 恢複
videoRecorder.onresume = (e) => {};
// 監聽錯誤
videoRecorder.onerror = (err) => {console.log(err); isRecordVideo = false;};
}
// video資料轉dataurl
function videoChunks2DataUrl (videoChunks) {
return new Promise((resolve, reject) => {
let blob = new Blob(videoChunks, {type: 'video/webm'})
let fileReader = new FileReader()
fileReader.readAsDataURL(blob)
fileReader.onloadend = (data) => { resolve(data) }
fileReader.onerror = (err) => { reject(err) }
})
}
// 開始錄像
function startRecordVideo() {
if (isRecordVideo) return;
videoRecorder.start(600000);
isRecordVideo = true;
}
// 停止錄像
function stopRecordVideo() {
if (!isRecordVideo) return;
videoRecorder.stop();
isRecordVideo = false;
}
// 下載下傳檔案
function downloadFileByA (from) {
let ele = document.createElement('a')
ele.download = '1.webm'
ele.style.display = 'none'
ele.href = from
document.body.appendChild(ele)
ele.click()
document.body.removeChild(ele)
}
// 下載下傳音頻檔案
function downloadAudio () {
if ((window.myAudioData || []).length === 0) return
let _myAudioData = mergeArrayBuffer(window.myAudioData)
let blob = new Blob([_myAudioData], {type: 'application/octet-stream'})
window.URL = window.URL || window.webkitURL
let ele = document.createElement('a')
ele.href = window.URL.createObjectURL(blob)
ele.download = 'myAudio.mp3'
ele.click()
window.URL.revokeObjectURL(ele.href)
}
// 合并多個ArrayBuffer 或 TypeArray
function mergeArrayBuffer(arrays) {
let totalLen = 0
for (let i = 0; i < arrays.length; i++) {
arrays[i] = new Uint8Array(arrays[i]) //全部轉成Uint8Array
totalLen += arrays[i].length
}
let res = new Uint8Array(totalLen)
let offset = 0
for(let arr of arrays) {
res.set(arr, offset)
offset += arr.length
}
return res.buffer
}
實際操作的時候,需要在适當的時候使用以下代碼進行控制操作。
// 下載下傳步驟:
// 1、擷取canvas資料流
canvasObj = document.querySelector('.plv__screen--canvas');
mediaStream = canvasObj ? canvasObj.captureStream() : null;
// 2、初始化MediaRecorder
initVideoRecorder();
// 3、開始錄像
startRecordVideo();
// 4、停止錄像
stopRecordVideo();
音頻
因為音頻使用的是 AudioContext,下載下傳起來不太友善。後來,經過一番調試,發現在 audioDecoder.feed 處可以得到解密後的音頻資料(資料類型為arraybuffer)。于是,在該地方添加一段代碼,将接收到的音頻資料全都儲存到一個全局變量中,等視訊播放完成後将這些資料儲存到本地。
下載下傳音頻
以上方法,實操起來比較笨拙,後續會實踐其它思路。