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

本文做者: IMWeb團隊 原文連接javascript

整體介紹

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

視頻點播分爲視頻上傳和視頻播放兩個部分,下面的表格整理了上雲先後的部分數據對比:前端

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

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

總體方案

考慮到存量視頻較多,無法短期內所有從騰訊視頻遷移至騰訊雲,同時遷移過程當中用戶可能繼續使用老的方式向騰訊視頻上傳,因此整個點播上雲分爲兩期進行:git

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

就是幹

視頻上傳流程

錄播上傳流程

視頻上傳總體方案如上圖所示,主要涉及三塊:github

  1. 向業務後臺獲取簽名
  2. 調用雲SDK 進行視頻上傳
  3. 雲服務器進行視頻轉碼

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

另外,其實上述流程圖與騰訊雲文檔給出的客戶端上傳指引略微有點差異,主要在於第 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;
}
複製代碼

寫在最後

雖然在上雲的過程當中遇到了一些問題,但都能順利地解決,並且最後的產品數據與用戶體驗都比以前有了提高,但願愈來愈多業務能積極地擁抱雲的時代!

相關文章
相關標籤/搜索