以下为本人实际工作中经验所得分享,
日常项目中涉及到实时视频流播放,大都会选择flvJs,后者videoJs。而由于这两款无法满足实际需求并且无法解码h265视频
流,所以在后端C++的配合下,一起写了一套自用的视频流播放器,视频解码使用的是libffmpeg,找不到资源的可以私信我,
原理就是利用wasm编写c++代码使用ffmpeg进行视频解码。由于算法解码压力,解码动作在浏览器完成对电脑及带宽都有一定要求。
视频播放我们采用了两种方式来实现,可以通过配置设置,一种是使用video播放mediaSource的流媒体,一种是使用webgl绘
制画面。mediaSource支持播放流媒体片段,这样播放器无需等待所有视频资源全都下载完再播放,可以不断的向播放器喂视
频流片段。
定义websockt连接类,管理信令及数据交互,分发事件
首先连接websocket,监听websockt事件及跟C++定义好将要发送的信令。
处理推送过来的数据,如果是二进制数据则为视频流,否则为信令通知
根据信令类型分发不同事件,其中说明一下slow跟fast是发送流的速率,来实现倍数播放,从8倍速到1/8倍速,mediachange,是切换mime类型,用来实现高清跟标清切换。
接受到当前视频流的mime类型,可以从mime类型中判断当前视频流的编码格式,avc则是h264, hevc则是h265,由于我们项目中只会存在这两种,所以我只简单的做了个判断。从代码第四行可以看到这里有做renderType的判断,是webgl绘制还是使用mediaSource,本文只讲述一下webgl模式,因为要实现0延时的人脸追踪跟车像追踪就需要使用这种模式。
定义对外基础类,包含播放器初始化,基础属性配置(倍速列表,缓冲区大小,播放类型等),播放器状态监听及异常分发。处理视频流数据
处理推送过来的视频流数据(说明一下,如果不需要实现人脸追踪的情况下,一般是每次推送一个完整的媒体片段,否则需要每一帧一帧的推送),我们和C++约定好视频流里的头部信息,这里粗略看下就好,主要包含版本信息,头部长度等
我们约定在28,29个字节存放当前帧里的ar数据的长度,如果为0则无数据,有则截取该长度的字节,解析ar数据。最后解析到的是一个ar对象的list,里面会描绘当前画面有多少个目标及分别的位置信息(当然如果能够解析出更多的信息用也可以同步推送过来,如人脸特征,是否有嫌疑信息,这对底层算法要求太高,实时解析压力大)。,
定义h264,h265解码类,因为此webgl模式下是由前端负责解码工作并绘制,c只负责推送裸流,如果使用mediaSource模式则是c将视频解码之后再推送过来。所以需要使用Decoder类,主要负责视频流队列管理,ar队列管理,定时解码,发布解码之后的数据
将媒体数据跟ar数据都存到响应的队列,等待解码绘制,方便解码完成之后找到对应帧的ar数据。
Wasm封装了ffmpeg插件,js引入之后向外抛出了一个大的Module对象,里面封装解码的相关方法,可以直接调用。Wasm内部是有C++代码构成的,目前本人也没有深入了解。这里我们不做深究,能用就行。到这一步的时候遇到一个问题,解码是异步的,解码之后怎么跟当前ar数据对应。
首先执行openDecoder打开解码器,该方法接受当前视频流类型h264还是h265,还有一个videoCallBack的回调函数,解码成功之后调用他,为了解决解码数据跟ar数据对应的问题,请当时C++同事查看了Wasm源码跟处理逻辑,发现pts字段本身代表时间刻度,但可以由外部传入自定义数据并回调的时候可以拿到该自定义数据(仅此字段可用)。所以我们在定时解码的时候自定义了个pts当做索引,每次解码则+1,刚好跟我们队列里的索引一一对应。接受到解码之后的数据则开始准备绘制,
视频数据跟ar数据分别在两个canvas上绘制,canvas本身是透明的,两者叠加在一起就可以,这样也方便对ar数据进行事件处理,比如细节人像的点击事件获取更多数据,或者人脸分析等。
最后就是webgl渲染类,主要负责处理解码之后的Yuv数据跟ar数据进行绘制,当然另外再抽离了一个webgl用的texture类,这里就不列出来了。就是标准的webgl纹理处理
export default class WebGLPlayer {
constructor(canvas, options, arCanvas) {
this.canvas = canvas;
this.gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
this.ctx = arCanvas.getContext("2d")
this.initGL(options);
}
initGL(options) {
if (!this.gl) {
console.log("[ER] WebGL not supported.");
return;
}
var gl = this.gl;
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
var program = gl.createProgram();
var vertexShaderSource = [
"attribute highp vec4 aVertexPosition;",
"attribute vec2 aTextureCoord;",
"varying highp vec2 vTextureCoord;",
"void main(void) {",
" gl_Position = aVertexPosition;",
" vTextureCoord = aTextureCoord;",
"}"
].join("\n");
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
var fragmentShaderSource = [
"precision highp float;",
"varying lowp vec2 vTextureCoord;",
"uniform sampler2D YTexture;",
"uniform sampler2D UTexture;",
"uniform sampler2D VTexture;",
"const mat4 YUV2RGB = mat4",
"(",
" 1.1643828125, 0, 1.59602734375, -.87078515625,",
" 1.1643828125, -.39176171875, -.81296875, .52959375,",
" 1.1643828125, 2.017234375, 0, -1.081390625,",
" 0, 0, 0, 1",
");",
"void main(void) {",
" gl_FragColor = vec4( texture2D(YTexture, vTextureCoord).x, texture2D(UTexture, vTextureCoord).x, texture2D(VTexture, vTextureCoord).x, 1) * YUV2RGB;",
"}"
].join("\n");
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log("[ER] Shader link failed.");
}
var vertexPositionAttribute = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(vertexPositionAttribute);
var textureCoordAttribute = gl.getAttribLocation(program, "aTextureCoord");
gl.enableVertexAttribArray(textureCoordAttribute);
var verticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0]), gl.STATIC_DRAW);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]), gl.STATIC_DRAW);
gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
gl.y = new Texture(gl);
gl.u = new Texture(gl);
gl.v = new Texture(gl);
gl.y.bind(0, program, "YTexture");
gl.u.bind(1, program, "UTexture");
gl.v.bind(2, program, "VTexture");
};
- 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.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
初始化webgl,有兴趣深入了解webgl的可以看下webgl编程指南这本书,大概意思就是绘制了YUV三种纹理,在顶点着色器里定义一个变量存储纹理坐标,在片段着色器里定义了3个纹理像素拾取器(sampler2D)因为视频解码之后返回过来的就是yuv的数据信息,对于前端人员可能对RGB比较数据,YUV是一种颜色编码方法,像小时候的黑白电视机就只有Y数据。
开始绘制,注意绘制ar数据的时候需要根据当前画布大小跟原视频大小对比,计算出正确的ar位置。每次绘制视频画面之前先把ar的内容清空。至此基本介绍就完成了。
注意细节
-
如果是回放则需要把没有播放完的片段保留在队列,直播则直接舍弃seek到最新的点
-
mediaSource的sourceBuffer.mode记得设置为sequence,此配置意味着video将按照buffer队列依次播放,不会根据buffer的时间戳来播放。
【本文正在参加云原生有奖征文活动】,活动链接:https://ost.51cto.com/posts/12598
大佬牛逼,写得非常好,收货大大的
mark,现在AI场景下,对视频结合AI分析数据做实时渲染的场景越来越多了。