问题引入
视频的第一帧加载缓慢。
简介
HTTP Live Streaming,缩写为HLS,是由苹果公司提出基于HTTP的流媒体网络传输协议。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的扩展 M3U (m3u8)播放列表文件,用于寻找可用的媒体流。
目前HLS协议被广泛的应用于视频点播和直播领域。
由Apple提出和开发,但并不是仅仅使用在ios系统上,目前在各种终端均得到了支持
M3U文件是一种纯文本文件,可以指定一个或多个多媒体文件的位置
使用场景
HLS与MP4格式的对比
视频格式 | 优点 | 缺点 | MP4 | 各种设备及服务端、CDN都通用 | 头文件较大,边下载边缓存,起播慢 拖动时间轴播放时,需要一定的时间缓存 | HLS | 对视频进行切片,按切片播放,缓存小,起播快 拖动时间轴到任意时间播放时,可以快速定位到对应的切片进行播放,响应快 | 长视频会导致分片较多 |
总结
长视频的大文件头影响加载速度的视频体验,所以短视频适合使用mp4。
hls将整个视频流分成一个个小的基于Http的文件进行下载播放,因此支持视频点播和直播。
原理与使用
原理介绍
HLS 跟 DASH 协议的原理非常类似。通过将整条流切割成一个小的可以通过 HTTP 下载的媒体文件,然后提供一个配套的媒体列表文件,提供给客户端,让客户端顺序地拉取这些媒体文件播放,来实现看上去是在播放一条流的效果。由于传输层协议只需要标准的 HTTP 协议,HLS 可以方便的透过防火墙或者代理服务器,而且可以很方便的利用 CDN 进行分发加速,并且客户端实现起来也很方便。
HLS 把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。HLS 协议由三部分组成:HTTP、M3U8、TS。这三部分中,HTTP 是传输协议,M3U8 是索引文件,TS 是音视频的媒体信息。
具体实现
七牛云在视频上传相关提供了相应的语言SDK供给前后端调用,但七牛提供给前端上传视频的上传策略较为单一,生成七牛云上传token的方式在JSSDK中是通过拼接完成的,只能实现简单的视频上传逻辑。因此视频上传很多策略只能在后端指定。
而如果在后端实现整个视频上传的流程的话,在视频上传过程中,服务器的荷载会比较大,并且线上的nginx服务器拒绝了20M以上的文件上传请求,因此大视频上传选择了前后端结合的方法,由后端生成七牛云的上传token并指定相应的上传策略后,由前端使用token进行文件的上传。
前端实现
七牛JS SDK?
import qiniu from 'qiniu-js'
/**
* 上传视频到七牛云
* @property { string } key 上传的目标空间 + 文件资源名
* @property { string } token 上传验证信息,前端通过接口请求后端获得
* @property { File } file 待上传的文件
* @property { Object<string, string> } customVars 上传过程中带上的自定义变量,详见 https://developer.qiniu.com/kodo/1235/vars
* @property { Object<string, Function> } handlers 上传过程的监听函数,有三个属性 next、error、complete
*/
const qiniuUpload = ({
key,
token,
file,
customVars = {},
handlers = {}
}) => {
const observable = qiniu.upload(
file,
key,
token,
{
customVars
}
)
observable.subscribe(handlers)
}
const processUploadVideo = async (file) => {
// 从后端拿取到上传七牛的token
const { key, token } = await getQiniuToken()
qiniuUpload({
key,
token,
file,
handlers: {
next(e) {
// 处理上传进度
const percentage = parseInt(e.total.percent)
// ...
},
error(e) {
// 视频上传失败后
},
/**
* 视频上传成功
* @param { Object } uploadResult 上传完成后的后端返回信息,具体返回结构取决于后端sdk的配置
*/
complete(uploadResult) {
const { key, hash } = uploadResult
// ...
},
}
})
}
后端实现
七牛云音视频切片接口文档
视频传输的后端操作:
composer require qiniu/php-sdk
在使用SDK的过程中鉴权是很重要的一块,不管是上传鉴权,下载签权, 还是回调的签权。 PHP SDK 中的 Auth 类封装了所有的鉴权方式。 所以在使用 PHP SDK 时基本都会先对鉴权类进行初始化:
<?php
require 'path_to_sdk/vendor/autoload.php';
use Qiniu\Auth;
// 用于签名的公钥和私钥
$accessKey = 'Access_Key';
$secretKey = 'Secret_Key';
// 初始化签权对象
$auth = new Auth($accessKey, $secretKey);
在上传类 UploadManager 中主要负责文件的上传, 文件上传分为两种上传方式:表单上传和分片上传。当然在PHP SDK中你不用关心这个细节,只需要调用 UploadManager 中的上传方法即可。UploadManager 中的上传方法会根据上传文件的大小选择不同的上传方式,小于4MB的进行表单上传,大于4MB的进行分片上传。 以下一个简单的示例使你更清晰地了解如何初始上传对象:
但因为服务器荷载的问题,后端只负责实现带有文件上传策略的token,文件上传的操作在客户端执行。
require 'path_to_sdk/vendor/autoload.php';
use Qiniu\Auth;
use Qiniu\Storage\UploadManager;
$accessKey = 'Access_Key';
$secretKey = 'Secret_Key';
$auth = new Auth($accessKey, $secretKey);
$bucket = 'Bucket_Name';
// 生成上传Token
$token = $auth->uploadToken($bucket);
// 构建 UploadManager 对象
$uploadMgr = new UploadManager();
具体代码实现:
/**
* 获取上传到七牛云的token
*
* @return array
*/
public function video()
{
#构造鉴权类
$auth = new Auth($this->accessKey, $this->secretKey);
#以时间戳命名文件,防止部分中文特殊符号URL无法解析
$time = strtotime(date('Y-m-d H:i:s'));
#设置mp4文件的存储地址
$key = '****/'.$time.".mp4";
#设置m3u8文件的存储地址/需要以bucket:存储空间作为地址
$save_key = "bucket:****/".$time.".m3u8";
#m3u8的文件存储地址作为saveas参数,需要base64转码存储
$save_key = Qiniu\base64_urlSafeEncode($save_key);
#使用HLS流文件的专用队列进行异步转码
$pipeline = "m3u8_hls";
#文件转码的命令,将文件转为码率较高,清晰度较高,最为贴近源文件的模式
#文件转码为m3u8格式,分为10s每段的切片,静态码率为128比特/s,音频采样频率为44100Hz,音频编码方案为libfaac,视频帧率为30帧
#视频比特率为640bit/s,视频编码方案为libx264,不清除文件的metadata,空间另存为saveas(不设置则为随意的编码)
$pfopOps = "avthumb/m3u8/segtime/10/ab/128k/ar/44100/acodec/libfaac/r/30/vb/640k/vcodec/libx264/stripmeta/0|saveas/".$save_key;
#设定上传文件后七牛云的返回值
$returnBody = [
'key' => "$(key)",
'hash' => "$(etag)",
'persistentId' => "$(persistentId)",//七牛云魔法变量设置返回值新增persistentId
];
#设定七牛云转码后的回调接口(需要是可以被正常Post方法https访问且状态码为200的接口路径)
$callback_url = "*****";
#将returnBody转化为json格式输出
$returnBody = json_encode($returnBody);
#设定后端上传策略
$policy = array(
'persistentOps' => $pfopOp5s,
'persistentPipeline' => $pipeline,
'returnBody' => $returnBody,
'persistentNotifyUrl' => $callback_url,
);
# 生成上传Token
$token = $auth->uploadToken($this->bucket, $key, 3600, $policy, true);
}
视频在上传成功后,七牛云会在上传文件的同时异步进行视频文件的转码工作,转码操作往往比上传操作要慢上很多,此时让用户等待视频转码完成后再保存无疑很不现实,那么前端保存视频资源时就固定保存MP4文件,在转码完成后设置回调接口再将视频资源的路径改为m3u8格式。同时如果上传的视频文件很小,在用户保存之前就完成了视频的转码工作,在数据库中无法检索到相应的MP4文件的资源,因此无法实现视频资源由MP4向m3u8的转换,因此需要前端在保存资源时调用视频转码状态查询的接口,传入转码的任务ID来查询转码是否完成,若已完成转码,则将视频资源由MP4转换为m3u8。
require 'path_to_sdk/vendor/autoload.php';
use Qiniu\Auth;
use Qiniu\Processing\PersistentFop;
#构造鉴权类
$auth = new Auth($accessKey, $secretKey);
#获得前端所传的任务ID等参数
$persistentId = $_data['persistentId'];
$mp4_url = $_data['mp4_url'];
$m3u8_url = $_data['m3u8_url'];
#构造七牛设置类
$config = new \Qiniu\Config();
#进行任务的状态查询
$pfop = new PersistentFop($auth, $config);
list($ret, $err) = $pfop->status($persistentId);
if ($err != null) {
return ajaxCallback(0, '任务id无法查询');
} else {
#code为上传任务的状态,items-code为转码任务的状态,同时完成时才算视频任务的完成
if ($ret['code'] != 0 || $ret['items']['code'] != 0) {
return ajaxCallback(0, '视频任务尚未完成');
} else {
#任务完成时,从数据库中拿取对应mp4_url的视频资源的课程
$sql = "select `id` from `kx_course` where `source_url` = '{$mp4_url}' and `del` = 0";
$course = new Course();
$course_id = $course->db()->fetchOne($sql);
#若未查询到,则不执行操作
if (!$course_id) {
return ajaxCallback(0, '无对应MP4_url的课程');
} else {
#查询到则将mp4视频资源修改成m3u8的视频资源
$update_arr = [
'source_url' => $m3u8_url,
];
$course->db()->update('kx_course', $update_arr, "`id` = {$course_id}");
#将课程id返回给前端进行校验,防止课程资源修改错误
return ajaxCallback(1, '成功将视频对应资源修改为HLS格式', ['course_id' => $course_id]);
}
}
}
- 转码回调接口的代码实现
#所传参数以数据流的形式传入,需要以这种方式接收
$_body = file_get_contents('php://input');
#传入参数是json格式,需要json解码为数组
$_body = json_decode($_body, 1);
#视频资源的空间路径,拼接上key值即为完整的视频资源
$qiniu_url = "****";
#需要注意有转码时,body传入时的文件key参数名为inputKey(坑点)
if (!empty($_body['inputKey'])) {
$mp4_url = $qiniu_url.$_body['inputKey'];
$sql = "select `id` from `kx_course` where `source_url` = '{$mp4_url}' and `del` = 0";
$course = new Course();
$course_id = $course->db()->fetchOne($sql);
if (!$course_id) {
echo json_encode([
'error' => 0,
'url' => $qiniu_url.$_body['inputKey']
]);
die();
} else {
#取不出来items中的key值,将mp4文件的后缀名改为m3u8后存入数据库
$m3u8 = explode(".",$_body['inputKey']);
$m3u8_url = $qiniu_url.$m3u8[0].".m3u8";
$update_arr = [
'source_url' => $m3u8_url
];
$course->db()->update('kx_course', $update_arr, "`id` = {$course_id}");
}
echo json_encode([
'error' => 0,
'url' => $qiniu_url.$_body['key']
]);
die();
} else {
echo json_encode([
'error' => 1,
'message' => '视频上传出错'
]);
}
?
踩坑点:
- 生成上传token时,key是原MP4文件的存储地址,而不是转码后m3u8文件的地址,这点文档没有写清楚。
- 文件上传时,最好将文件名修改一下,否则用户原文件名可能带有一些特殊字符如空格,中文符号等转码时无法被解析导致视频无法播放。
- 设置转码后文件存储地址saveas时,变量需要以bucket:{存储空间}作为变量的值,并且变量需要通过七牛自带SDK的base64加密后才能生效,否则七牛云会生成一个随机字符串作为转码后文件的存储地址。
- 转码时最好设置一个专属队列进行视频的转码操作,与普通上传区分开来,这样转码的速度可以大大提升。
- 设定上传策略时,需要在returnBody中自定义七牛云的返回魔法变量,这样上传后七牛云会返回需要的字段以进行后续的转码状态查询。
- 七牛云调用回调接口时,所传入的参数是以数据流形式传入的json字符串,需要json转码成为数组,且同时有转码和上传任务是,参数中的key参数名为inputKey,需要注意一下。
参考资料
- HTTP Live Streaming
- 七牛云音视频切片接口文档
- 七牛云PHPSDK
- DASH、HLS和MP4格式视频有什么区别?
|