Camera 和 MediaCodec 对视频旋转角度的处理
共 7542字,需浏览 16分钟
·
2023-11-01 10:17
Surface 和 GLConsumer 对视频旋转角度的处理
最近遇到一个有趣的问题:通过MediaCodec
解码带旋转角度的视频时,如果Output Surface是TextureView或者SurfaceView提供的,那么屏幕上的视频帧可以正常展示(处理了旋转角度);如果Surface是由SurfaceTexture创建而来,那么通过SurfaceTexture获取的OES纹理,是未处理旋转角度的,需要自己兼容下角度问题。
此外,我也测试了Camera
,首先通过Camera.setDisplayOrientation
设置顺时针旋转角度,然后设置Output Surface接收Camera预览视频帧。不出意外,也得到了同样的结论。
视频旋转角度的处理
既然都是通过Surface
接收视频帧,那为什么会存在这种差异那?MediaCodec
和Camera
生产视频帧,并且展示到屏幕上的流程如下所示:
首先,
MediaCodec
和Camera
会通过native_window_set_buffers_transform
函数向Surface(ANativeWindow)设置Transform Flag
,保存在Surface.mTransform
变量。接着,生产端
MediaCodec
和Camera
通过Surface持有的BufferQueueProducer
向BufferQueue入队BufferItem时,会把Surface.mTransform
赋值给BufferItem.mTransform
。然后,消费端
GLConsumer
通过BufferQueueComsumer
获取BufferItem后,根据BufferItem.mTransform
,还原出一个4*4的纹理变换矩阵,保存在GLConsumer.mCurrentTransformMatrix
变量中,GLConsumer
的使用者可以通过GLConsumer.getTransformMatrix
方法获取这个纹理变换矩阵。最后,
GLConsumer
的使用者负责把纹理变换矩阵作用到纹理上,以正确展示视频帧。
GLConsumer
负责把BufferItem.mGraphicBuffer
转换为纹理,并且根据BufferItem.mTransform
计算出纹理变换矩阵,此时的纹理是原始状态(未应用纹理矩阵)。获取纹理变换矩阵,并且应用到纹理,是GLConsumer
(纹理)使用方的责任。
不管Surface
是由TextureView提供还是通过SurfaceTexture
创建,上述前3步都是相同的流程。导致上述差异的根本原因是第4步:GLConsumer的使用者是否把纹理变换矩阵应用到纹理
当Surface
由TextureView
提供时,GLConsumer
的使用者是DeferredLayerUpdater
,它获取纹理变换矩阵后,会填充到Layer.texTransform
变量(frameworks\base\libs\hwui\Layer.h),后续在硬件加速的异步渲染线程中,OpenGLRenderer
渲染DrawLayerOp
(基于上面的Layer
创建而来)时,会应用该纹理变换矩阵。
当Surface
由SurfaceView
提供时,Surface
是独立窗口,GLConsumer
的使用者是frameworks/native/services/surfaceflinger/Layer,它获取纹理变换矩阵后,会填充到Layer.mTexture.mTextureMatrix中,后续SurfaceFlinger
合成Layer时,会应用该纹理变换矩阵。
可见,当Output Surface由TextureView或者SurfaceView提供时,GLConsumer
的使用者主动获取并使用了纹理变换矩阵,所以我们在屏幕上看到的视频帧才是正常的。
而当Output Surface由我们基于OES纹理ID创建的SurfaceTexture
创建而来时,GLConsumer
(纹理)的使用者是业务方,所以需要业务方通过SurfaceTexture.getTransformMatrix
主动获取纹理变换矩阵,并把它应用到OES纹理上。即:我们拿到的OES纹理本就是原始纹理,需要应用纹理变换矩阵后,才能正常展示。
业务方要怎么使用纹理变换矩阵那?
首先,通过
SurfaceTexture.getTransformMatrix
获取纹理变换矩阵,该矩阵是列优先次序存储,可以直接通过glUniformMatrix4fv
函数上传到顶点着色器。在顶点着色器中,使用纹理变换矩阵左乘纹理坐标,并把最新的纹理坐标传递给片元着色器。
在片元着色器中,正常使用纹理坐标从纹理中取色就可以了(视频帧正确展示)。
视频旋转角度的流转流程
我们看下旋转角度
在MediaCodec
和Camera
中的流转流程。
MediaCodec
通过MediaCodec
解码视频时,需要通过MediaFormat
设置解码参数,例如:SPS、PPS等。当视频存在旋转角度时,需要通过MediaFormat.KEY_ROTATION
配置旋转角度,表示视频帧需要顺时针旋转多少度,才能正确展示。但是要注意的是:只有当MediaCodec
直接解码到Surface
时,旋转角度才有效。
旋转角度在MediaCodec中的流转流程如下所示:
MediaCodec::configure
配置解码器,MediaFormat作为参数。MediaCodec发送
kWhatConfigure
消息,在自有线程配置解码器。调用
ACodec->initiateConfigureComponent
(MediaFormat为参数)配置ACodec。ACodec发送
kWhatConfigureComponent
消息在自有线程配置ACodec。接着是
ACodec::LoadedState::onConfigureComponent
方法。然后是
ACodec.configureCodec
方法,负责通过MediaFormat配置ACodec,其中从format中取出了"rotation-degrees",保存在ACodec.mRotationDegrees
变量中。最后,
ACodec.setupNativeWindowSizeFormatAndUsage
中调用全局函数setNativeWindowSizeFormatAndUsage
为Surface(ANativeWindow)设置transform flag
。核心代码在setNativeWindowSizeFormatAndUsage函数中:
// 根据旋转角度获得transform flag
int transform = 0;
if ((rotation % 90) == 0) {
switch ((rotation / 90) & 3) {
case 1: transform = HAL_TRANSFORM_ROT_90; break;
case 2: transform = HAL_TRANSFORM_ROT_180; break;
case 3: transform = HAL_TRANSFORM_ROT_270; break;
default: transform = 0; break;
}
}
// 为Surface(ANativeWindow)设置transform
err = native_window_set_buffers_transform(nativeWindow, transform);
native_window_set_buffers_transform
函数会触发ANativeWindow子类Surface的perform方法处理NATIVE_WINDOW_SET_BUFFERS_TRANSFORM
消息,接着是dispatchSetBuffersTransform -> setBuffersTransform,最后把transform保存在了Surface.mTransform
变量中。
至此,旋转角度已经设置到了Surface.mTransform
,后面就是Surface生产图像数据时负责使用它了。
Surface内部通过BufferQueueProducer
入队图像数据时,会把Surface.mTransform
赋值给BufferItem.mTransform
,BufferQueue的元素就是BufferItem
。
然后,消费端GLConsumer
通过BufferQueueComsumer
获取BufferItem
后,根据BufferItem.mTransform,还原出一个4*4的纹理变换矩阵,保存在GLConsumer.mCurrentTransformMatrix
变量中,GLConsumer
的使用者可以调用GLConsumer.getTransformMatrix
方法获取这个纹理变换矩阵,然后在使用对应纹理时,把该矩阵应用到纹理上。
Camera
旋转角度在Camera中的流转流程如下所示:
Camera.setDisplayOrientation
设置预览显示时的顺时针旋转角度。对应Native方法android_hardware_Camera_setDisplayOrientation。
接着走到
Camera.sendCommand
方法,这里的Camera是客户端,会通过Binder调用到服务端CameraClient::sendCommand
。接着走到CameraClient::sendCommand设置orientation。
接着继续走到CameraHardwareInterface.setPreviewTransform
然后通过
native_window_set_buffers_transform
为Camera的Preview Window(Surface)设置旋转角度。最后就是Surface负责使用
Surface.mTransform
了,这部分流程与MediaCodec类似。
应该说MediaCodec和Camera设置旋转角度的流程不同,但是最终都是把旋转角度设置到Surface.mTransform
,后面就是Surface和GLConsumer的事情了。
总结
不管是MediaCodec
还是Camera
,都是Surface
的图像数据来源,只不过这个源图像数据,可能有一定的旋转角度或者镜像。这种情况下,MediaCodec
和Camera
就会通过native_window_set_buffers_transform
为Surface设置Transform Flag
,保存在Surface.mTransform
变量中,表示生成的图像数据,必须经过Transform变换之后才能正常显示。可用的Transform Flag如下所示:
// Transform Flag
typedef enum android_transform {
// 水平镜像
HAL_TRANSFORM_FLIP_H = 0x01,
// 垂直镜像
HAL_TRANSFORM_FLIP_V = 0x02,
// 顺时针旋转90度
HAL_TRANSFORM_ROT_90 = 0x04,
// 顺时针旋转180度
HAL_TRANSFORM_ROT_180 = 0x03,
// 顺时针旋转270度
HAL_TRANSFORM_ROT_270 = 0x07,
// don't use. see system/window.h
HAL_TRANSFORM_RESERVED = 0x08,
} android_transform_t;
生产端Surface
通过BufferQueueProducer
添加图像数据时,会把Surface.mTransform
赋值给BufferItem.mTransform
,然后入队到BufferQueue。
消费端GLConsumer
通过BufferQueueComsumer
获取BufferItem后,根据BufferItem.mTransform Flag
,还原出一个4*4的纹理变换矩阵,保存在GLConsumer.mCurrentTransformMatrix
变量中,GLConsumer
的使用者调用GLConsumer.getTransformMatrix
获取这个纹理变换矩阵,然后作用到对应纹理上,就可以正确展示视频帧了。
原文链接: https://juejin.cn/post/6844904195535929351
-- END --
进技术交流群,扫码添加我的微信:Byte-Flow
获取相关资料和源码
推荐:
全网最全的 Android 音视频和 OpenGL ES 干货,都在这了
面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?
项目疑难问题解答、大厂内部推荐、面试指导、简历指导、代码指导、offer 选择建议、学习路线规划,可以点击找我一对一解答。