OpenGLES开发透明通道视频备忘录

最近在做带透明MP4视频的渲染播放,原理就是视频的一半是只有黑白颜色的透明通道,另一半则是视频原来的画面,只要利用OpenGLES在渲染时提取原来画面的对应位置上的透明通道的透明度,设置到原来画面的像素上,就可以达到播放透明视频的效果。

原理很简单,实现也不难,但是中间遇到了一些坑,折腾了一段时间才弄明白,主要原因还是基础原理不了解,还是要多学习。

1.像素采样位置偏差

比如,一个视频的尺寸是1500x750,黑白透明通道在左边,原画面在右边,则实际渲染出来的视频大小应该是750x750。

OpenGL中,纹理的坐标原点在左下角,所以黑白透明通道的像素采样范围是(0,0)~(749,749),原画面的像素采样范围是(750,0)~(1499,749)。因为OpenGLES中采样器接收的像素坐标是0~1的浮点型,所以换算过来约等于是(0.0,0.0)~(0.5,1)和(0.5,0.0)~(1.0,1.0)。

所以刚开始时,我在fragment shader中的写法很简单,将vertex shader传入的纹理坐标的x直接减去0.5,即可获得透明通道的像素值,根据这个值换算成透明度即可。但是在我的Mate30 Pro上,实际效果并不理想,在原画面的物体边缘可以看到有黑边。

为什么会这样呢?我第一时间认为这是采样问题。

线性过滤和最临近过滤

我们称纹理的像素数据为纹素(texel),由于texel和pixel是无法一一对应的,即无论如何都会存在放大或缩小的情况,这个时候就需要通过对纹理采样来进行纹理像素到屏幕像素的映射。

具体说明:

所以我们知道,假如OpenGL中的图元和纹理大小完全一致,那么texel和pixel将完全对应,这时候就不需要采样了,但这是不可能的。基于传入的纹理坐标去采样texel时,由于放大或者缩小的关系,可能会采样到目标texel的隔壁去,或者一次采样到多个texel,那么实际渲染到图元上时,要么失真要么锯齿化。

那么,假如我们对纹理精确采样,是不是就能解决这个问题呢?

texelFetch

texelFetch是OpenglES 3.0提供的新方法,用于精确采样texel,和texture方法不同,传入的纹理坐标是整型的,因此采样出来的texel也不会经过过滤处理。

改为texelFetch后,问题确实解决了。但实际效果并不理想,因为没有过滤,在屏幕上的显示是马赛克(锯齿)化的,像素之间的颜色不连贯。

而且我后来在魅族pro7上测试,发现使用texture方法却不会出现边缘黑边。这说明texelFetch并不是解决方法。

H264硬解码的宏块与SurfaceTexture的变换矩阵

一般情况下,在Android中解码视频都会选择硬解码,速度快,效率高,节省资源。硬解码视频无非两种方式,使用MediaCodec手动解码或者MediaPlayer直接播放。两种方式都需要传入一个SurfaceTexture进去。

先说H264解码,在解码出一幅图像时,图像由N个宏块组成。宏块是H264中编解码的基本单位,一般一个宏块的大小都是16x16。

在Android上,H264硬编码、解码在一般情况下也是基于这个宏块大小为单位的,所以会有一个16位对齐的要求。在一些机型上,如果编码的视频尺寸没有对齐16位(即无法被16整除),最终出来的视频可能会花屏、绿屏之类的。而解码则不同,假如实际传入的视频不满足16为对齐,最终解码出来的图像尺寸,是原来尺寸除以16后向上取整再乘于16后得到的最临近原来视频尺寸、可被16整除的数值。

比如上述例子中的1500x750的视频,这个大小是无法被16整除的,所以实际渲染出来的视频大小,其实是:

ceil((1500,750)/16)*16 = (1504,752)

因此,最终解码并传入到纹理单元中的纹理尺寸,其实不是我们以为的1500x750,而是1504x752。前面说到在vertex shader中,传入的纹理坐标范围是(0.0, 1.0),假如我们不对坐标进行转换处理,最终渲染出来的图像是小于屏幕上设置的控件尺寸的。

所以我们需要SurfaceTexture的getTransformMatrix()方法,获取解码后经过变换的矩阵。这个变换矩阵的用处在于将传入的纹理采样坐标转换为真正的采样坐标。比如上述的1500x750的视频,解码输出为1504x752,那么变换矩阵是:

$$S_x=1500\div1504\approx0.997340$$
$$S_y=750\div752\approx0.997340$$
$$M=\begin{bmatrix}
S_x\\
S_y\\
1
\end{bmatrix} \times I_4 = \begin{bmatrix}
0.997340&0&0&0\\
0&0.997340&0&0\\
0&0&1&0\\
0&0&0&1
\end{bmatrix}$$

也就是说,要采样对应位置的透明通道texel,不能在转换了坐标之后才-0.5,需要先-0.5再进行转换,同时需要考虑到浮点型的精度,因为OpenGLES相对OpenGL的精度更低,无法准确判断一些边界值。

最终通过上述方法后,渲染出来的画面终于没有了边缘黑边,同时也没有了锯齿马赛克,达到理想效果。至于为什么在魅族pro7上硬解码出来的数据没有对齐16位,这个就不好说了,应该是各家底层有改造过。

2.SurfaceTexture销毁问题

根据项目需求,我这边使用的是TextureView,通过内部给出的SurfaceTexture进行OpenGLES绘制。视乎需求,可能会需要长期保存这个SurfaceTexture,但是这样会导致一些问题,比如onDetachedFromWindow()中需要把这个SurfaceTexture释放掉,但是一般没有很好的解决方法。网上推荐的是使用ijkPlayer内的TextureView的保存SurfaceTexture方式,不过比较复杂。

最好的方式其实是不保存SurfaceTexture,让TextureView自己管理。然后自己在每次触发onSurfaceTextureAvailable()和onSurfaceTextureDestroyed()方法时,再进行对应的逻辑处理。

3.Android4.4的兼容性问题

一开始我使用的是MediaCodec自行解码,但发现对应的native内存、graphics内存使用都有点偏高。后面使用MediaPlayer后,发现内存占用都很好,所以就一直使用了。但是发现在Android4.4下有兼容性问题。

  1. MediaPlayer在离开当前页面时(比如onPause或者onDestroy())必须销毁,不然会出现在新页面无法使用的情况。
  2. MediaPlayer存在底层状态异常的问题,有的机型一旦出现问题后,必须重启才能继续使用
  3. MediaPlayer无法在同一界面同时使用多个,具体能同时播放多少个不好说,这个好像有随机性

为了解决这个问题,最终还是在Android4.4下使用MediaCodec。使用华为荣耀畅玩x4测试,在同一界面上可以稳定同时使用两个进行解码播放,从第三个开始就会出现异常,所以无法完全避免这种兼容性问题。

Linux下罗技优联鼠标的简略指北

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×