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接收视频帧,那为什么会存在这种差异那?MediaCodecCamera生产视频帧,并且展示到屏幕上的流程如下所示:

  1. 首先,MediaCodecCamera会通过native_window_set_buffers_transform函数向Surface(ANativeWindow)设置Transform Flag,保存在Surface.mTransform变量。

  2. 接着,生产端MediaCodecCamera通过Surface持有的BufferQueueProducer向BufferQueue入队BufferItem时,会把Surface.mTransform赋值给BufferItem.mTransform

  3. 然后,消费端GLConsumer通过BufferQueueComsumer获取BufferItem后,根据BufferItem.mTransform,还原出一个4*4的纹理变换矩阵,保存在GLConsumer.mCurrentTransformMatrix变量中,GLConsumer的使用者可以通过GLConsumer.getTransformMatrix方法获取这个纹理变换矩阵。

  4. 最后,GLConsumer的使用者负责把纹理变换矩阵作用到纹理上,以正确展示视频帧。

GLConsumer负责把BufferItem.mGraphicBuffer转换为纹理,并且根据BufferItem.mTransform计算出纹理变换矩阵,此时的纹理是原始状态(未应用纹理矩阵)。获取纹理变换矩阵,并且应用到纹理,是GLConsumer(纹理)使用方的责任。

不管Surface是由TextureView提供还是通过SurfaceTexture创建,上述前3步都是相同的流程。导致上述差异的根本原因是第4步:GLConsumer的使用者是否把纹理变换矩阵应用到纹理

SurfaceTextureView提供时,GLConsumer的使用者是DeferredLayerUpdater,它获取纹理变换矩阵后,会填充到Layer.texTransform变量(frameworks\base\libs\hwui\Layer.h),后续在硬件加速的异步渲染线程中,OpenGLRenderer渲染DrawLayerOp(基于上面的Layer创建而来)时,会应用该纹理变换矩阵。

SurfaceSurfaceView提供时,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纹理本就是原始纹理,需要应用纹理变换矩阵后,才能正常展示。

业务方要怎么使用纹理变换矩阵那?

  1. 首先,通过SurfaceTexture.getTransformMatrix获取纹理变换矩阵,该矩阵是列优先次序存储,可以直接通过glUniformMatrix4fv函数上传到顶点着色器。

  2. 在顶点着色器中,使用纹理变换矩阵左乘纹理坐标,并把最新的纹理坐标传递给片元着色器。

  3. 在片元着色器中,正常使用纹理坐标从纹理中取色就可以了(视频帧正确展示)。

视频旋转角度的流转流程

我们看下旋转角度MediaCodecCamera中的流转流程。

MediaCodec

通过MediaCodec解码视频时,需要通过MediaFormat设置解码参数,例如:SPS、PPS等。当视频存在旋转角度时,需要通过MediaFormat.KEY_ROTATION配置旋转角度,表示视频帧需要顺时针旋转多少度,才能正确展示。但是要注意的是:只有当MediaCodec直接解码到Surface时,旋转角度才有效。

旋转角度在MediaCodec中的流转流程如下所示:

  1. MediaCodec::configure配置解码器,MediaFormat作为参数。

  2. MediaCodec发送kWhatConfigure消息,在自有线程配置解码器。

  3. 调用ACodec->initiateConfigureComponent(MediaFormat为参数)配置ACodec。

  4. ACodec发送kWhatConfigureComponent消息在自有线程配置ACodec。

  5. 接着是ACodec::LoadedState::onConfigureComponent方法。

  6. 然后是ACodec.configureCodec方法,负责通过MediaFormat配置ACodec,其中从format中取出了"rotation-degrees",保存在ACodec.mRotationDegrees变量中。

  7. 最后,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中的流转流程如下所示:

  1. Camera.setDisplayOrientation设置预览显示时的顺时针旋转角度。

  2. 对应Native方法android_hardware_Camera_setDisplayOrientation。

  3. 接着走到Camera.sendCommand方法,这里的Camera是客户端,会通过Binder调用到服务端CameraClient::sendCommand

  4. 接着走到CameraClient::sendCommand设置orientation。

  5. 接着继续走到CameraHardwareInterface.setPreviewTransform

  6. 然后通过native_window_set_buffers_transform为Camera的Preview Window(Surface)设置旋转角度。

  7. 最后就是Surface负责使用Surface.mTransform了,这部分流程与MediaCodec类似。

应该说MediaCodec和Camera设置旋转角度的流程不同,但是最终都是把旋转角度设置到Surface.mTransform,后面就是Surface和GLConsumer的事情了。

总结

不管是MediaCodec还是Camera,都是Surface的图像数据来源,只不过这个源图像数据,可能有一定的旋转角度或者镜像。这种情况下,MediaCodecCamera就会通过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 FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

抖音传送带特效是怎么实现的?

所有你想要的图片转场效果,都在这了

面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?

我用 OpenGL ES 给小姐姐做了几个抖音滤镜


项目疑难问题解答、大厂内部推荐、面试指导、简历指导、代码指导、offer 选择建议、学习路线规划,可以点击找我一对一解答。

浏览 719
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报