内容简介:视频点播分为视频上传和视频播放两个部分,下面的表格整理了上云前后的部分数据对比:可以看出来上传成功率和视频转码速度有了极大的提升,PC 和 H5 侧的播放成功率云和腾讯视频基本持平。
总体介绍
腾讯课堂 是一款通过线上的直播与点播向用户提供在线教育服务的产品,从 2014 年成立至今,已累计存储了 250 万个视频,共 600 TB,累计时长 150 万小时。之前一直采用的是腾讯视频的方案,但使用的是 MP4 格式,用户拿到了播放链接之后很容易盗版,所以趁着上云的潮流,我们将视频点播迁移到了 腾讯云 - 云点播 上,本文主要会讲一讲我们 整体的方案、Web 接入的方法和遇到的一些问题 。
视频点播分为视频上传和视频播放两个部分,下面的表格整理了上云前后的部分数据对比:
腾讯视频 | 腾讯云 | |
---|---|---|
Web 视频上传成功率 | 92% | 99.5% |
视频转码速度(两小时左右的视频) | > 60 分钟 | < 20 分钟 |
播放成功率 - PC | 99% | 98.7% |
播放成功率 - H5 | 97% | 97.1% |
可以看出来上传成功率和视频转码速度有了极大的提升,PC 和 H5 侧的播放成功率云和腾讯视频基本持平。
整体方案
考虑到存量视频较多,没法短时间内全部从腾讯视频迁移至腾讯云,同时迁移过程中用户可能继续使用老的方式向腾讯视频上传,所以整个点播上云分为两期进行:
- 第一期主要工作是接入腾讯云的上传、转码和播放功能,确保用户新上传的视频均走云的流程,同时后台将新上传的视频旁路一份到腾讯视频,这样既可以在用户播放云视频失败时前端降级至腾讯视频播放,也方便出现重大问题时快速切回至老的腾讯视频方案。
- 第二期工作则是将存量的腾讯视频全部迁移至腾讯云上,同时接入云的 AI 功能,进行鉴黄、鉴暴和鉴政。待现网数据稳定且达到预期后,即可彻底摒弃老的方案。
视频上传流程
视频上传整体方案如上图所示,主要涉及三块:
- 向业务后台获取签名
- 调用云SDK 进行视频上传
- 云服务器进行视频转码
上面三块中最重要也最容易出问题的是"调用 SDK 上传"这一部分,直接决定了上传成功率,但也很容易受用户网络状况的影响,需要重点关注,建议记录详细的用户日志以便进行问题定位与排查。
另外,其实上述流程图与腾讯云文档给出的 客户端上传指引 略微有点差别,主要在于第 4 步通知业务后台上传完成这里,官方文档中是云后台来通知,我们实际采用的方式是 Web 侧来通知,从而避免出现 Web 侧调后台接口出错提示用户上传失败后,云后台又通知业务后台保存相关数据的情况。
视频播放流程
在以前使用腾讯视频的方案时,出于种种考虑,我们并未对视频做加密处理,导致有些课程被他人恶意盗录。目前上云之后,我们使用的是加密 HLS 的方案,通过云提供的 Key 防盗链 和 DRM(数字版权管理)方案 ,我们对视频做了加密处理,就算被拿到了视频地址,也无法进行盗录,进一步打击了恶意行为,保护了老师的版权。
用户浏览器在播放视频时主要流程如上图所示,其中依靠第 1 步获取 Token 和第 3 步获取 DK 进行版权的保护,他们的作用分别为:
- Token 用于防盗链,可以 限制视频 URL 的过期时间、最大允许播放 IP 数等,具体的计算方法和验证逻辑由业务方自定义。
- DK 用于对视频的加密切片进行解密,用户直接获取到的视频分片均通过 AES-128 进行了加密,其值由腾讯云密钥管理服务(KMS)提供。
Web 接入的流程
视频上传
接入方法
视频上传主要依赖云提供的 vod-js-sdk-v6 ,用 TypeScript编写,具有较为完善的的测试用例,代码质量很高 :+1: 其底层依赖的是 cos-js-sdk-v5 ,也是由腾讯云提供的对象存储能力。
接入 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);
虽然上传的 SDK 用起来很简单,但在我们灰度的过程中,还是遇到了一些问题,因而强烈建议在代码中加入详细的上报日志,例如上面的 DEMO 中可以加入的日志信息包括:获取签名的开始、成功与失败,文件上传的开始、成功与失败等。
遇到的问题
1. 默认只开启了重庆存储区
上线后我们发现视频上传的链接均是 xxx.cos.ap-chongqing.myqcloud.com
的形式,这看起来不太对呀,怎么都往 chongqing(重庆区)上传了呢?难道不支持就近上传的能力吗?后来我们联系云的同事得知,由于视频云的底层依赖的是腾讯云的对象存储(COS),所以具体往哪传,怎么传比较快是由 COS 保证的,需要在云控制台开启相关配置。
2. SDK 上传部分报错
上传初期进行灰度时发现上传成功率为 97%,距离预期的 99% 还存在一定距离,通过双方的合作排查,最终发现主要是由两个问题引起的:
- 用户本地时间与服务器时间不一致时,依赖的 cos-js-sdk-v5 鉴权报错,导致出现 403;
- 用户网络抖动时,云视频的 vod-js-sdk-v6 对签名的处理存在问题,导致出现 403。
目前在最新版的 vod-js-sdk-v6
中上述问题均已解决,上传成功率在全量后也在 99.5% 以上。
PC & H5 视频播放
前面已经简单提过了视频播放流程,我们这里再来详细说明一下。
流程简介
点播播放其实很简单,简单来说就是下面这个流程:
第一步: 获取 m3u8
地址
第二步:调用播放器播放
就是这么简单。
这时候我们发现一个问题,有了 m3u8
地址,所有人都能播放了。这个 m3u8
地址可以肆无忌惮的传播,任何人拿到链接都可以播放,就没有付费课的概念了。于是我们开始引入前面提到的第一个技术,我们称之为 Key 防盗链 。防盗链参数是动态变化的,引入之后我们的流程就变成了:
加了防盗链之后,缺少防盗链参数的链接就没法播放了。就算带防盗链参数的 m3u8
地址传播出去,因为有时效性,这个链接过一阵子也会失效。
这时候,聪明的小伙伴应该又发现了另外一个问题,假设在防盗链参数失效之前把 m3u8
文件下载下来,一样是可以拿来传播的。
要解决这个问题,我们可以简单来看下 m3u8
的格式。
简单的说, m3u8
是一个遵循某种格式的文本文件,里面是一些 TS
分片的索引,通过这些索引就可以找到所有的视频分片。
回到我们加密的主题,如果是每一个 TS
分片做加密,是不是就算把 m3u8
下载下来,也没法播放了呢? HLS
的普通 AES 加密技术正是这样做的。引入了 HLS
普通加密之后,整个流程就变成了这样:
为了简单起见,我们忽略了 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
格式里用 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
则用于出现了播放失败等情况时的排查。
小程序视频播放
小程序端有两个问题需要解决:
- 腾讯云并没有提供可用的云播放组件供前端使用,所以需要我们自己封装一个组件,提供云视频播放能力;
- 小程序没有cookie,而且m3u8文件获取解密密钥的方法是由video自动完成的,代码无法控制,所以小程序端只能采用 QueryString 传递身份认证信息 的方案去鉴权;
我们先来看一下小程序组件腾讯云视频播放的一个基本流程:
- 课堂这边是开启了防盗链和HLS加密的,所以上述的判断流程都走绿色的路径;
- tokenObj 是防盗链的token,里面包括: 播放地址的过期时间戳、试看时长、链接标识、防盗链签名。参考 Key 防盗链 ;
- drmToken 是m3u8获取解密密钥需要用到的鉴权token,具体规则由前后端在业务层约定加密规则。参考 QueryString 传递身份认证信息 ;
-
<cloud-player-video />
组件内部的播放还是用的小程序的<video />
组件,只是提供了通过参数获取真正播放地址的功能; - 目前
<cloud-player-video /\>
是我们自己研发的组件,还在持续迭代优化中,后续会加入倍速切换,清晰度切换等播放器常用功能;
- 小程序端通过业务的cgi拿到对应的fileId,然后通过getCloudUrlToken的接口获取对应的 tokenObj ;
- 通过登录接口获取的内容经过加密生成 drmToken 用以解密时的鉴权;
- 结合对应腾讯云业务的 appid 以及获取到的 tokenObj 、 drmToken 、 fileId 这四个关键参数传递给云播放组件
<cloud-player-video />
; - 在组件内部利用 appid 、 tokenObj 、 fileId 这三个参数可以到腾讯云拿到加密的m3u8地址(通过getPlayInfo),然后利用 drmToken 信息附加到原始 m3u8 地址上(通过getUrlToken);
- 将新的 m3u8 地址传递给小程序的video组件,获取到的 m3u8 文件内部就会将 drmToken 的信息注入到 EXT-X-KEY 字段的URI中,以 QueryString 的方式传递,最终 drmToken 将会注入到 m3u8 文件内,图片上面已经贴过,再贴一遍
- video组件会自动读取这个URI去拿到解密的密钥将TS文件解密然后进行播放;
课堂小程序中获取 tokenObj 、 drmToken ,由于这两个参数的获取方式是业务决定的,内部流程就不赘述了,贴一下的步骤代码:
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是根据 appid 、 tokenObj 、 fileId 获取原始 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; }
写在最后
虽然在上云的过程中遇到了一些问题,但都能顺利地解决,而且最后的产品数据与用户体验都比之前有了提升,希望越来越多业务能积极地拥抱云的时代!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 干货 | 如何搭建小型视频点播网站
- 基于django的视频点播网站开发-step4-首页功能
- 基于django的视频点播网站开发-step5-详情页功能
- 权利的游戏、破冰行动都烂尾了,那就来讨论一下视频点播吧
- MeEdu 0.1.4 发布,基于 Laravel 开发的在线视频点播系统
- 如何实现歌曲在线点播?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。