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";
var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) { var mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener("sourceopen", sourceOpen); } else { console.error("Unsupported MIME type or codec: ", mimeCodec); } function sourceOpen(_) { var mediaSource = this; var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); fetchAB(assetURL, function (buf) { sourceBuffer.addEventListener("updateend", function (_) { mediaSource.endOfStream(); video.play(); }); 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);
xhr.responseType = "blob";
xhr.onload = function (e) { if (this.status == 200) { var blob = this.response; vidElement.src = URL.createObjectURL(blob); } }; xhr.send();
|
说了这么多原理就是这么点。直接把 C#吐出来的二进制流丢去创建对象连接,然后做为视频 src,这样等于前端缓存了一道,视频进度拖动和倍数都是操作前端缓存,跟 mp4 源无关。
改进后的问题
读取视频列表后,有在视频间切换的需求,不管是油管还是 B 站,不管是手动还是自动,就会面临 Blob 的替换,要是它们那样直接跳页面又是全新的还好做,但是摸索怎么不 Load 页面的情况下切掉 Video 的 Blob 而不是操作同一个 Blob 是个方向。
参考连接