CodeMaker

world change code

原生H5音视频回播方案优化

Flash 的时代逐渐被 HTML 不断更新的协议替代,但是目前原生 Video 确实不好用,有不少前端插件在努力辅助完善,比如 Video.js 和 Flv.js,但是没有看到太多前后端联动的方案,用我们录制视频回播案例记一下。

案例

我们针对不同的 IP Camera 按时间分段录制了不少监控视频存储在服务器磁盘,按时间规则排序提供回放。支持分段大小自定义,那么读取最好支持流式。之前简单粗暴上 Video 标签,发现问题:

  • 流式请求了两次,一次拉大小,一次读。
  • 因为 video 直接怼过去是 mp4 流,没法 control 进度,一拖就归 0 重拿。
  • 进度不能 control 设 playbackRate 倍数直接卡。
    这个临时赶的演示被客户吐槽了,拖也拖不了,倍数设置不了,后面要上生产,必需摸索解决掉。

这里的回放区别于 m3u8,和 nginx+rtmp 的点播。后面有空往这个方向看看

摸索日志

奔着 Blob 去

油管和 B 站现在的视频播放方案都很成熟,我直播用的 Flv.js 还是 B 站开源的,那么他们的播放源 Blob 是怎么做的?
– 查关键字 create video blob url一圈,并没有相关介绍。倒是不少怎么去收集 Blob 形成 mp4 视频下载的操作贴。
“渐渐误入歧途”,比如这篇,在前端做异步去拿视频流,然后拼成 Blob 数组丢到 createObjectURL,理论上看起来 OK,略显粗暴。继续挖发现了MediaSource接口,那整个原理的 Demo 应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var video = document.querySelector("video");
var assetURL = "frag_bunny.mp4";
// Need to be specific for Blink regarding codecs
// ./mp4info frag_bunny.mp4 | grep Codec
var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) {
var mediaSource = new MediaSource();
//console.log(mediaSource.readyState); // closed
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener("sourceopen", sourceOpen);
} else {
console.error("Unsupported MIME type or codec: ", mimeCodec);
}
function sourceOpen(_) {
//console.log(this.readyState); // open
var mediaSource = this;
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetchAB(assetURL, function (buf) {
sourceBuffer.addEventListener("updateend", function (_) {
mediaSource.endOfStream();
video.play();
//console.log(mediaSource.readyState); // ended
});
sourceBuffer.appendBuffer(buf);
});
}
function fetchAB(url, cb) {
console.log(url);
var xhr = new XMLHttpRequest();
xhr.open("get", url);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
cb(xhr.response);
};
xhr.send();
}

迅速做一段代码跑来看看,然后失败了!这么简单清晰的逻辑找来找去对 video/mp4; codecs="avc1.42E01E, mp4a.40.2"产生了怀疑,前端编解码出了问题跟服务吐出来的貌似对不上。查到了这篇问题这篇科普发现不对劲,如果这么专业的啃就超纲了,一时半会儿解决不了问题。
就一个 mp4 做二进制流丢出来,整个 Blob 到 Url 对象提供 Src 这么难?开始从前后端从新捋。

确认 C#接口

做了两个接口,一个提供视频列表,一个提供视频流式读取(记得开启 API 跨域):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[HttpGet]
public IActionResult GetRecordList(string ip)
{
var basePath = Path.Combine(HostingEnvironment.WebRootPath, "record/");
var savePath = $"{basePath}{ip}";
if (!Directory.Exists(savePath))
return Content(null, "application/json");
var files = new DirectoryInfo(savePath).GetFiles();
var fileList = files.Select(x =>
{
var arr = x.Name.Split('_');
var time = (arr[0] + ":" + arr[1]);
var _t = Convert.ToDateTime(time);
var sec = Convert.ToInt32(arr[2].Split('.')[0]);
var lon = (sec / 60) + ":" + (sec % 60);
return new
{
Long = sec,
Date = _t.ToString("yyyy-MM-dd"),
Time = _t.ToString("HH:mm") + "-" + _t.AddMinutes(sec / 60).ToString("HH:mm"),
FileName = x.Name,
FilePath = x.FullName
};
});
return Content(fileList.ToJson(), "application/json");
}

忽略哪些文件处理冗余代码先
流式读取:

1
2
3
4
5
[HttpGet]
public IActionResult GetVideo(string path)
{
return PhysicalFile(path, "application/octet-stream");
}

确认前端实现

确定在 vue 里直接用Axios封装直接去异步拿视频并没有什么问题,但是记得约定 HTTP Header:responseType: 'blob',为了避免干扰,我从新开了一个测试页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var vidElement = document.querySelector("video");
var xhr = new XMLHttpRequest();
var videoUrl = "https://127.0.0.1:5001/api/record/getvideo?path=D:\\StreamApi\\wwwroot\\record\\192.168.1.10\\2020-08-20T17_31_60.mp4";
//配置请求方式、请求地址以及是否同步
xhr.open("GET", videoUrl, true);
//设置请求结果类型为blob
xhr.responseType = "blob";
//请求成功回调函数
xhr.onload = function (e) {
if (this.status == 200) {
//请求成功
//获取blob对象
var blob = this.response;
//获取blob对象地址,并把值赋给容器
vidElement.src = URL.createObjectURL(blob);
}
};
xhr.send();

说了这么多原理就是这么点。直接把 C#吐出来的二进制流丢去创建对象连接,然后做为视频 src,这样等于前端缓存了一道,视频进度拖动和倍数都是操作前端缓存,跟 mp4 源无关。

改进后的问题

读取视频列表后,有在视频间切换的需求,不管是油管还是 B 站,不管是手动还是自动,就会面临 Blob 的替换,要是它们那样直接跳页面又是全新的还好做,但是摸索怎么不 Load 页面的情况下切掉 Video 的 Blob 而不是操作同一个 Blob 是个方向。

参考连接