H264视频在线转码播放
条评论H264视频在线转码播放
事情是这样的,后端开发人员说,通过websocket转发客户端发送的h264视频到前端,前端播放视频。我一听就知道这个需求不简单。首先,我知道只有少数的几种视频格式能在浏览器里播放,如果不能播放就涉及到转码的问题。其次,我没试过在websocket里传二进制的文件,传过来的数据到底要怎么处理才能像HTTP GET到的文件一样进行处理。
这里要特别说明一下,一般来说大家并不会遇到这种需求,因为一般是不会把大量计算放到前端来运行,而是交由后端运算完再发给前端,前端只是个界面展示,不应该用作大量计算导致卡顿。(服务器的性能会优于用户的电脑,同时要照顾机器性能差的用户。)除非你这个页面就是打算用来占用用户的性能来挖矿的…..而提出这个需求的后端开发人员,是个技术一般般的老员工,但话语权比我高,所以我也只能先做出来试试看,再不济就展示出来说性能的确不行,要改方案由后端来做,真不是我平白无故给你增加工作量,而确实是应该由后端来做。
先放结论
- 正确的做法应该是由后端进行转码,转成浏览器可直接播放的格式再发送到前端,由前端播放。毕竟正常情况下,CPU密集计算终究不适合在前端做,而适合在后端做。
- websocket是可以传二进制文件,大多数的做法是
websocket.binaryType = 'arraybuffer'
,只有图片是可以使用base64,因为浏览器可以直接使用base64字符串来展示图片。
ffmpeg.js
我搜了一下,大多数的解决方案都需要后端处理,没找到纯前端的解决方案,直到我找到ffmpeg.js。ffmpeg是一个转码器,有人开发了一套环境可编译JS,所以我就来试试。并且找了一些ffmpeg相关的入门进行学习,先在window下使用ffmpeg试着玩几下。
缺少Decoder
当我使用时ffmpeg.js,发现提示的是Decoder (codec msmpeg4v2) not found for input stream
,就是说ffmpeg.js里缺少了msmpeg4v2
这个东西导致无法将我的视频进行转码。我看到了有原文件的编译指南,我将Makefile里的DECODERS加入msmpeg4v2,然后进行编译:1
2# 原本是:COMMON_DECODERS = vp8 h264 vorbis opus mp3 aac pcm_s16le mjpeg png
COMMON_DECODERS = vp8 msmpeg4v2
按指南使用docker编译:1
2docker run --rm -it -v /path/to/ffmpeg.js:/mnt -w /opt kagamihi/ffmpeg.js
cp -a /mnt/{.git,build,Makefile} . && source /root/emsdk/emsdk_env.sh && make && cp ffmpeg*.js /mnt
一开始我是扔到了我的腾讯云主机(1核 2GB)上编译的,编着编着看到报错信息并且就挂掉了,怀疑是性能不够,后来我就在公司的台式电脑里跑虚拟机再次编译并成功。再试使用经我编译的ffmpeg.js时就可以正常转码了,转成webm格式。
在线转码并播放:<video controls="" preload="auto" id="_video"></video>
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
37
38
39
40const f= async ()=>{
const videoData = await axios.get('/input.h264', { responseType: 'blob' }).then(res => {
return res.data.arrayBuffer()
})
const videoDataBuffer = new Uint8Array(videoData)
const worker = new Worker("ffmpeg-worker-webm.js");
worker.onmessage = function(e) {
const msg = e.data;
switch (msg.type) {
case "ready":
worker.postMessage({
type: "run",
MEMFS: [{ name: 'test.h264', data: videoDataBuffer }],
arguments: ['-i', 'test.h264','-c:v', 'libvpx', 'out.webm']
});
break;
case "stdout":
console.log(msg.data);
break;
case "stderr":
console.log(msg.data);
break;
case "done":
console.log(msg.data);
var blob = new Blob([msg.data.MEMFS[0].data], {type: 'video/ogg'});
var url = URL.createObjectURL(blob);
console.log(blob,url)
document.getElementById('_video').src = url;
setTimeout(() => {
console.log('删除线程')
worker.terminate()
}, 1000);
break;
}
};
}
f()
在线转码慢
ffmpeg.js
是同步处理的,一个4秒(25fps,共100帧)的视频,等差不多两分钟才转码完毕,寻思也太久了吧。使用worker不知道会不会好一些, 然后使用ffmpeg-worker-webm.js
,发现处理起来是一帧一秒,所以大概100秒才能转码完毕。但我在windows下使用ffmpeg转这个视频,基本上是1秒就转码完毕了,这差距也太大了吧。反复搜索,直到最后也还没找到原因,不知道是因为无法多线程导致的问题(编译成JS时是禁掉了多线程的功能,worker本身不支持,看issue讨论好像搞成WebAssembly能支持多线程),还是转成JS就是很慢(因为我用nodejs跑也是很慢,说明不是浏览器独有的问题),反正我是暂时没时间也没必要再继续研究下去了。
我寻思,如果我同时开10个worker,第一个worker转0-10帧,第二个worker转11-20帧,如此类推,每帧1S的话,10个同时跑可能10S就转完了。使用'-ss'
与'-frames'
这两个参数,决定从哪里开始转,转多少帖,就可以实现这个功能了。测试时发现,转码时间从100秒降低到60秒,并不是我所想的10秒,估计是线程太多了CPU也应付不过来,我测试是4个线程左右就是我手提电脑的极限了,在chrome任务管理器里可以看到我这个测试页面CPU最高达400%多,(之前使用单个worker是100%多),估计是到达了我电脑的极限。而且分成10个文件转码,最后还得要将这10个文件再次通过ffmpeg合并起来,才是一个完整的视频。
做到这里,发现前端转码还是太慢了,给大家展示一下就交由后端转码就可以了,已经达到我的目的,不再深入研究。
解码base64二进制文件
果然,后端开发人员在通过websocket发送二进制文件,跟图片一样,是通过base64进行编码再发送过来。经过搜索,其实正确的做法应该是websocket.binaryType = 'arraybuffer'
。我接收后,怎么转码都不能正常使用:1
2
3
4window.btoa('china is so nb') // 编码
"Y2hpbmEgaXMgc28gbmI="
window.atob("Y2hpbmEgaXMgc28gbmI=") // 解码
"china is so nb"
最后花了大量时间各种踩坑,各种搜索尝试(通过HTTP GET文件,Blob,Text ,与websocket各种对比踩坑)。最后我尝试将一个二进制文件自行通过btoa
来编码,发现超出了ASCII
字符串而报错(Uncaught (in promise) DOMException: Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.
),终于我意识到是解码的问题,必须要兼容超出ASCII
。搜索尝试发现,js-base64可解决问题:1
const data = Base64.toUint8Array(fileBase64String)