天天看點

Polyv視訊下載下傳 · 初探

作者:xue5hen
聲明:文章内容僅供學習交流。

通過開發者工具檢視網站,不難發現,Polyv視訊是使用canvas标簽來渲染視訊畫面的,音頻是通過AudioContext進行播放的。

此外,網站的視訊是加密的,如果想要下載下傳視訊,比較好的辦法是找出視訊資料的解密算法,然後去解密恢複出視訊資料。但這個思路比較費時費力,一時半會很難有進展。是以這篇文章裡,我們先來嘗試一個實踐比較簡單的思路。因為視訊和音頻是分開的,是以我們依次來分析。

視訊

因為視訊是使用的是canvas标簽,而canvas繪制畫面的常見方法包括:drawImage(2d)、putImageData(2d)、drawArrays(webgl)。

Polyv視訊下載下傳 · 初探

搜尋圖檔繪制方法

1)首先全局搜尋這幾個方法(總共隻搜到4個),均打上斷點;

Polyv視訊下載下傳 · 初探

打斷點

2)在網頁中播放視訊,看哪個斷點會随着視訊的播放不斷被觸發;

通過調試發現,頻繁觸發的是drawArrays方法,在代碼的其它地方也可以發現webgl的字樣。視訊渲染之是以使用webgl,應該是因為它的性能要比2d快好幾倍。

3)将canvas繪制畫面時使用的圖檔資料下載下傳到本地;

在上一步中,既然已經開始使用drawArrays渲染畫面了,那麼此時的資料必然已經是解密後的資料,是以我們在該方法的地方插入代碼,将圖檔資料直接下載下傳到本地;

Polyv視訊下載下傳 · 初探

下載下傳圖檔

因為新增的代碼是同步操作,是以,在邊播放邊儲存的過程中,會導緻視訊播放不流暢,體驗不好;

觀察下載下傳後的圖檔,每張圖檔大概有1M多。

Polyv視訊下載下傳 · 初探

檢視圖檔

4)使用MediaRecorder來下載下傳視訊資料

因為上一步中的下載下傳方式體驗較差,是以我嘗試使用MediaRecorder來替代。在文檔中可以看到,MediaRecorder支援傳入來自canvas的資料流。

Polyv視訊下載下傳 · 初探

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)。于是,在該地方添加一段代碼,将接收到的音頻資料全都儲存到一個全局變量中,等視訊播放完成後将這些資料儲存到本地。

Polyv視訊下載下傳 · 初探

下載下傳音頻

以上方法,實操起來比較笨拙,後續會實踐其它思路。

繼續閱讀