【騰訊課堂】視訊點播上雲實踐

騰訊IMWeb團隊發表於2019-06-27

本文作者: IMWeb團隊 原文連結

總體介紹

騰訊課堂是一款通過線上的直播與點播向使用者提供線上教育服務的產品,從 2014 年成立至今,已累計儲存了 250 萬個視訊,共 600 TB,累計時長 150 萬小時。之前一直採用的是騰訊視訊的方案,但使用的是 MP4 格式,使用者拿到了播放連結之後很容易盜版,所以趁著上雲的潮流,我們將視訊點播遷移到了騰訊雲 - 雲點播上,本文主要會講一講我們整體的方案、Web 接入的方法和遇到的一些問題

視訊點播分為視訊上傳和視訊播放兩個部分,下面的表格整理了上雲前後的部分資料對比:

騰訊視訊 騰訊雲
Web 視訊上傳成功率 92% 99.5%
視訊轉碼速度(兩小時左右的視訊) > 60 分鐘 < 20 分鐘
播放成功率 - PC 99% 98.7%
播放成功率 - H5 97% 97.1%

可以看出來上傳成功率和視訊轉碼速度有了極大的提升,PC 和 H5 側的播放成功率雲和騰訊視訊基本持平。

整體方案

考慮到存量視訊較多,沒法短時間內全部從騰訊視訊遷移至騰訊雲,同時遷移過程中使用者可能繼續使用老的方式向騰訊視訊上傳,所以整個點播上雲分為兩期進行:

  1. 第一期主要工作是接入騰訊雲的上傳、轉碼和播放功能,確保使用者新上傳的視訊均走雲的流程,同時後臺將新上傳的視訊旁路一份到騰訊視訊,這樣既可以在使用者播放雲視訊失敗時前端降級至騰訊視訊播放,也方便出現重大問題時快速切回至老的騰訊視訊方案。
  2. 第二期工作則是將存量的騰訊視訊全部遷移至騰訊雲上,同時接入雲的 AI 功能,進行鑑黃、鑑暴和鑑政。待現網資料穩定且達到預期後,即可徹底摒棄老的方案。

就是幹

視訊上傳流程

錄播上傳流程

視訊上傳整體方案如上圖所示,主要涉及三塊:

  1. 向業務後臺獲取簽名
  2. 呼叫雲SDK 進行視訊上傳
  3. 雲伺服器進行視訊轉碼

上面三塊中最重要也最容易出問題的是"呼叫 SDK 上傳"這一部分,直接決定了上傳成功率,但也很容易受使用者網路狀況的影響,需要重點關注,建議記錄詳細的使用者日誌以便進行問題定位與排查。

另外,其實上述流程圖與騰訊雲文件給出的客戶端上傳指引略微有點差別,主要在於第 4 步通知業務後臺上傳完成這裡,官方文件中是雲後臺來通知,我們實際採用的方式是 Web 側來通知,從而避免出現 Web 側調後臺介面出錯提示使用者上傳失敗後,雲後臺又通知業務後臺儲存相關資料的情況。

視訊播放流程

在以前使用騰訊視訊的方案時,出於種種考慮,我們並未對視訊做加密處理,導致有些課程被他人惡意盜錄。目前上雲之後,我們使用的是加密 HLS 的方案,通過雲提供的 Key 防盜鏈DRM(數字版權管理)方案,我們對視訊做了加密處理,就算被拿到了視訊地址,也無法進行盜錄,進一步打擊了惡意行為,保護了老師的版權。

視訊播放流程

使用者瀏覽器在播放視訊時主要流程如上圖所示,其中依靠第 1 步獲取 Token 和第 3 步獲取 DK 進行版權的保護,他們的作用分別為:

  • Token 用於防盜鏈,可以 限制視訊 URL 的過期時間、最大允許播放 IP 數等,具體的計算方法和驗證邏輯由業務方自定義。
  • DK 用於對視訊的加密切片進行解密,使用者直接獲取到的視訊分片均通過 AES-128 進行了加密,其值由騰訊雲金鑰管理服務(KMS)提供。

Web 接入的流程

視訊上傳

接入方法

視訊上傳主要依賴雲提供的 vod-js-sdk-v6,用 TypeScript 編寫,具有較為完善的的測試用例,程式碼質量很高 ? 其底層依賴的是 cos-js-sdk-v5,也是由騰訊雲提供的物件儲存能力。

接入 SDK 的方法很簡單,只涉及兩方面:

  1. 傳入獲取簽名的函式來初始化 SDK,SDK 會在需要時自動呼叫。目前來看,SDK 會在上傳前、上傳中以及上傳成功後各獲取一次簽名。
  2. 呼叫 SDK 的 upload 函式上傳視訊。
import TCVod from 'vod-js-sdk-v6';

// 用簽名函式觸發
const uploader = new TCVod({
  getSignature,
});

// 向業務後臺獲取簽名
function getSignature() {
  return fetch('FAKE_CGI_URL').then((result) => {
    return result.sign;
  })
}

// 呼叫 SDK 上傳
function uploadVideo(videoFile) {
  const upVideo = uploader.upload({ videoFile });
  upVideo.on('video_progress', (info) => {
    // 此處獲取上傳進度
    // 例如上傳百分比、上傳速度等
  });

  upVideo.done().then((result) => {
    // 此處獲取上傳結果
    // 例如 fileId、CDN 原始檔地址等
  }).catch((error) => {
    // 上傳失敗
  });
}

uploadVideo(fileA);
uploadVideo(fileB);
複製程式碼

so easy

雖然上傳的 SDK 用起來很簡單,但在我們灰度的過程中,還是遇到了一些問題,因而強烈建議在程式碼中加入詳細的上報日誌,例如上面的 DEMO 中可以加入的日誌資訊包括:獲取簽名的開始、成功與失敗,檔案上傳的開始、成功與失敗等。

遇到的問題

1. 預設只開啟了重慶儲存區

上線後我們發現視訊上傳的連結均是 xxx.cos.ap-chongqing.myqcloud.com 的形式,這看起來不太對呀,怎麼都往 chongqing(重慶區)上傳了呢?難道不支援就近上傳的能力嗎?後來我們聯絡雲的同事得知,由於視訊雲的底層依賴的是騰訊雲的物件儲存(COS),所以具體往哪傳,怎麼傳比較快是由 COS 保證的,需要在雲控制檯開啟相關配置。

COS 儲存區選擇

2. SDK 上傳部分報錯

上傳初期進行灰度時發現上傳成功率為 97%,距離預期的 99% 還存在一定距離,通過雙方的合作排查,最終發現主要是由兩個問題引起的:

  • 使用者本地時間與伺服器時間不一致時,依賴的 cos-js-sdk-v5 鑑權報錯,導致出現 403;
  • 使用者網路抖動時,雲視訊的 vod-js-sdk-v6 對簽名的處理存在問題,導致出現 403。

目前在最新版的 vod-js-sdk-v6 中上述問題均已解決,上傳成功率在全量後也在 99.5% 以上。

PC & H5 視訊播放

前面已經簡單提過了視訊播放流程,我們這裡再來詳細說明一下。

流程簡介

點播播放其實很簡單,簡單來說就是下面這個流程:

播放1

第一步: 獲取m3u8地址

第二步:呼叫播放器播放

就是這麼簡單。

這時候我們發現一個問題,有了m3u8地址,所有人都能播放了。這個m3u8地址可以肆無忌憚的傳播,任何人拿到連結都可以播放,就沒有付費課的概念了。於是我們開始引入前面提到的第一個技術,我們稱之為Key 防盜鏈 。防盜鏈引數是動態變化的,引入之後我們的流程就變成了:

播放2

加了防盜鏈之後,缺少防盜鏈引數的連結就沒法播放了。就算帶防盜鏈引數的m3u8地址傳播出去,因為有時效性,這個連結過一陣子也會失效。

這時候,聰明的小夥伴應該又發現了另外一個問題,假設在防盜鏈引數失效之前把m3u8檔案下載下來,一樣是可以拿來傳播的。

要解決這個問題,我們可以簡單來看下m3u8的格式。

m3u8
m3u8

簡單的說,m3u8是一個遵循某種格式的文字檔案,裡面是一些TS分片的索引,通過這些索引就可以找到所有的視訊分片。

回到我們加密的主題,如果是每一個TS分片做加密,是不是就算把m3u8下載下來,也沒法播放了呢?HLS 的普通 AES 加密技術正是這樣做的。引入了HLS普通加密之後,整個流程就變成了這樣:

播放3

為了簡單起見,我們忽略了COS CDN 這一塊的圖示。解釋一下上圖:

首先是加密,要加密就要要金鑰。這時候就引入了KMS,我們暫時不關心KMS內部實現,簡單認為做了就是提供金鑰的工作。騰訊雲收到了業務後臺發起的視訊加密請求之後,就會從KMS 獲取對應的加密金鑰,對檔案進行加密處理。這就是上圖藍色字的部分。

然後是解密,業務前端在拿到m3u8的內容的時候,發現需要解密TS的,所以需要解密金鑰,於是就會請求業務後臺去獲得解密金鑰。業務後臺怎麼認為請求是合法的呢?當然是要有使用者的身份資訊(cookie)。騰訊雲提供了兩種方式,具體可以看HLS 普通加密 。上圖示例即是第一種方案,用例子來解釋一下。我們看一個 m3u8 地址示例:

https://1258712167.vod2.myqcloud.com/fb8e6c92vodtranscq1258712167/c896adc25285890789334843878/drm/voddrm.token.dWluPTt2b2RfdHlwZT0yO2NpZD00MDY4NDQ7dGVybV9pZD0xMDA0ODUxNzc7cHNrZXk9O2V4dD0=.v.f3071.m3u8?t=5d2f1647&exper=0&us=7776585111527298975&sign=195ed8bcbc08bb5e40f4823c49e71696

這裡的dWluPTt2b2RfdHlwZT0yO2NpZD00MDY4NDQ7dGVybV9pZD0xMDA0ODUxNzc7cHNrZXk9O2V4dD0=即是需要帶給業務後臺的鑑權token。再看看這個檔案的內容:

m3u8

m3u8格式裡用EXT-X-KEY 值用於解密,上圖的cgi-bin/qcloud/get_dk即是我們圖示裡的第 5 步,攜帶身份資訊,向業務後臺獲取解密金鑰。獲得解密金鑰之後,就可以對TS檔案解密並且播放啦~

程式碼實現

瞭解了流程之後,程式碼其實就很簡單了。

首先:獲取 m3u8 地址,並拼接上 token

async getM3U8List(fileId: string) {
  const { termId, onError } = this.props;
  try {
    // 獲取防盜鏈引數,對應流程圖裡第2步
    const urlParams = await getUrlToken({
      termId,
      fileId,
    });
    // 獲取 m3u8 地址,對應流程圖裡第3步
    const videoInfo = await getPlayInfo(fileId, urlParams);
    // 獲取拼接了 token 之後的 m3u8 地址
    const m3u8List = getPlayListWithToken(videoInfo, {
      termId,
    });

    return m3u8List;
  } catch (e) {
    onError(e);
  }
}
複製程式碼

其次,呼叫播放器,這裡可以參考超級播放器 或者 tcplayerlite。文件比較詳細,這裡就不贅述了。我們播放完整流程圖裡的第 4 步則是由播放器發起的,第 5 步由瀏覽器自己發起的。

播放質量監控

關於監控,播放目前是使用內部 monitor + tdw + badjs 上報做監控的。

monitor用於告警和資料累積量的檢視。

tdw用於報表、日報、週報的生成。

badjs則用於出現了播放失敗等情況時的排查。

小程式視訊播放

小程式端有兩個問題需要解決:

  1. 騰訊雲並沒有提供可用的雲播放元件供前端使用,所以需要我們自己封裝一個元件,提供雲視訊播放能力;
  2. 小程式沒有cookie,而且m3u8檔案獲取解密金鑰的方法是由video自動完成的,程式碼無法控制,所以小程式端只能採用QueryString 傳遞身份認證資訊的方案去鑑權;

我們先來看一下小程式元件騰訊雲視訊播放的一個基本流程:

weapp-process

  • 課堂這邊是開啟了防盜鏈和HLS加密的,所以上述的判斷流程都走綠色的路徑;
  • tokenObj 是防盜鏈的token,裡面包括: 播放地址的過期時間戳、試看時長、連結標識、防盜鏈簽名。參考Key 防盜鏈;
  • drmToken 是m3u8獲取解密金鑰需要用到的鑑權token,具體規則由前後端在業務層約定加密規則。參考QueryString 傳遞身份認證資訊
  • <cloud-player-video /> 元件內部的播放還是用的小程式的 <video /> 元件,只是提供了通過引數獲取真正播放地址的功能;
  • 目前 <cloud-player-video /\> 是我們自己研發的元件,還在持續迭代優化中,後續會加入倍速切換,清晰度切換等播放器常用功能;
  1. 小程式端通過業務的cgi拿到對應的fileId,然後通過getCloudUrlToken的介面獲取對應的 tokenObj
  2. 通過登入介面獲取的內容經過加密生成 drmToken 用以解密時的鑑權;
  3. 結合對應騰訊雲業務的 appid 以及獲取到的 tokenObjdrmTokenfileId 這四個關鍵引數傳遞給雲播放元件 <cloud-player-video />
  4. 在元件內部利用 appidtokenObjfileId 這三個引數可以到騰訊雲拿到加密的m3u8地址(通過getPlayInfo),然後利用 drmToken 資訊附加到原始 m3u8 地址上(通過getUrlToken);
  5. 將新的 m3u8 地址傳遞給小程式的video元件,獲取到的 m3u8 檔案內部就會將 drmToken 的資訊注入到 EXT-X-KEY 欄位的URI中,以 QueryString 的方式傳遞,最終 drmToken 將會注入到 m3u8 檔案內,圖片上面已經貼過,再貼一遍

m3u8

  1. video元件會自動讀取這個URI去拿到解密的金鑰將TS檔案解密然後進行播放;

課堂小程式中獲取 tokenObjdrmToken ,由於這兩個引數的獲取方式是業務決定的,內部流程就不贅述了,貼一下的步驟程式碼:

getCloudUrlToken(params)
.then(tokenObj => {
  const drmToken = getDrmToken({ term_id: termId });
  this.setData({
    fileId,
    appId: '1258712167', // pro
    drmToken,
    tokenObj,
  });
})
.catch(({ err_code, err_msg }) => {
  // 降級播放
  this.init(this.properties.playInfo, null, true);
});
複製程式碼

然後將四個關鍵引數傳遞給元件,如下:

<cloud-player-video
  player-id="course-video-player{{r}}"
  file-id="{{fileId}}"
  app-id="{{appId}}"
  token-obj="{{tokenObj}}"
  drm-token="{{drmToken}}"
  safety
  poster="{{poster && tools.renderUrl(poster)}}"
  bindplay="onPlay"
  bindpause="onPause"
  binderror="onVideoError"
  bindended="onEnded"
  bindmedianotsup="onMediaNotSup"![](http://imweb-io-1251594266.cos.ap-guangzhou.myqcloud.com/b645c306e5a3695be09104cfdb27183a.png)
></cloud-player-video>
複製程式碼

然後是 <cloud-player-video /> 元件內部的一些關鍵方法,getPlayInfo是根據 appidtokenObjfileId 獲取原始 m3u8 播放地址的方法;formatUrlWithToken是為 m3u8 地址附加drmToken的方法:

// 獲取視訊播放地址的方法
getPlayInfo() {
  const {
    fileId,
    appId,
    safety,
    tokenObj: {
      t,
      us,
      sign,
      exper = 0,
    },
  } = this.properties;
  // 當前版本預設獲取playInfo的地址
  let url = `https://playvideo.qcloud.com/getplayinfo/v2/${appId}/${fileId}`;
  // 如果開啟了防盜鏈,將防盜鏈資訊加到querystring裡面
  if (safety) {
    url += `?t=${t}&us=${us}&sign=${sign}&exper=${exper}`;
  }

  return request({ url });
}

// 附加drmToken的方法
formatUrlWithToken(m3u8 = '', drmToken) {
  const reg = /(\/drm\/)/g;
  let tokenUrl = m3u8.replace(/http:/, 'https:');
  tokenUrl = tokenUrl.replace(reg, `$1voddrm.token.${drmToken}.`);
  return tokenUrl;
}
複製程式碼

寫在最後

雖然在上雲的過程中遇到了一些問題,但都能順利地解決,而且最後的產品資料與使用者體驗都比之前有了提升,希望越來越多業務能積極地擁抱雲的時代!

相關文章