【腾讯课堂】视频点播上云实践

本文做者: 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;
}
复制代码

写在最后

虽然在上云的过程当中遇到了一些问题,但都能顺利地解决,并且最后的产品数据与用户体验都比以前有了提高,但愿愈来愈多业务能积极地拥抱云的时代!

相关文章
相关标签/搜索