Android Camera2 + OpenGL 竖屏或横屏预览会有“轻微拉伸”

前言

在进行 Android 相机底层开发(Camera2 + OpenGL ES)时,开发者经常会遇到各种拉伸问题。有一种最隐蔽的"轻微拉伸":画面方向正确,预览也没变黑,但人脸看起来明显比平时"瘦长"了一点点。

本文将结合一次真实的生产环境日志排查,解析其背后的硬件限制与逻辑陷阱。

现象描述

在开发一个 App 时,由于需要支持 竖屏模式(9:16) 下的高清视频采集,我们在设置中指定了 1080 x 1920 的视频分辨率。但在实测中,原本标准的 16:9 画幅却被纵向拉长了。

现场日志回放

我们可以看到设置分辨率和系统检测的过程:

复制代码
// 设置生效
2026-04-15 15:28:27.332  3580-3580  CameraController  D  Video resolution from settings: 1080x1920

// 关键硬件支持信息(从 characteristics 读出)
支持的录像尺寸 (PRIVATE): 4000x3000、3840x2160、3264x2448... 1920x1080、1280x720

// 关键点:渲染器初始化与方向同步
2026-04-15 15:28:27.333  3580-3580  CameraGLRenderer  D  GL renderer already initialized at 1080x1920, skipping
2026-04-15 15:28:27.333  3580-3580  CameraGLRenderer  D  Display rotation set: 0
2026-04-15 15:28:27.335  3580-3580  CameraGLRenderer  D  Sensor orientation set: 270
2026-04-15 15:28:27.339  3580-3580  CameraGLRenderer  D  Orientation offset set: 90

深入排查:真相只有一个

为什么设置了 1080x1920 会拉伸?

逻辑分析

在代码中,我们通常会告诉相机的缓冲区(SurfaceTexture)期望接收的分辨率:

复制代码
 // 创建相机纹理(OES外部纹理)  相机输入流
                cameraTextureId = TextureProgram.createExternalTexture()
                cameraTexture = SurfaceTexture(cameraTextureId).apply {
                 setDefaultBufferSize(height, width)

正常手机此时 videoSize 是 1080x1920。

真相揭秘

  1. 硬件层面的 Landscape 特性 :Android 设备的摄像头传感器(Sensor)在硬件物理层面上几乎全都是横向放置的。相机底层驱动能处理的原始流(Output Sizes)绝大多数只支持宽大于高的横向格式(如 1920x1080)。

  2. 不匹配的回退机制 :当我们向相机请求一个 1080(W) x 1920(H) 的输出流时,Camera2 HAL(硬件抽象层)由于在其支持列表中找不到对应的"竖屏"尺寸,它并不会报错,而是会执行回退策略

  3. 4:3 与 16:9 的战争 :通过日志发现,该设备最大支持 4000x3000(4:3比例)。因为驱动识别不了竖着的 16:9,它极大概率会选择传感器全像素尺寸或者一个默认的 4:3 预览尺寸(如 1440x1080)吐出数据。

  4. OpenGL 强制填满:你的预览窗口(GL Viewport)是真正的 1080x1920 (9:16)。OpenGL 在渲染时,如果不做裁切处理,会将一个 1440x1080 的图像内容"揉碎并挤压"进 1080x1920 的矩阵里。

数学上的畸变:

  • 原始比例(假设 4:3):1.33

  • 显示比例(期望 16:9 纵向):0.562 (即横向的 1.77)

  • 畸变倍数 = 1.77 / 1.33 = 1.33倍。 这就是你人脸看起来"瘦长"了 30% 左右的由来。

最终修复

修复这个问题的核心不是去旋转图像,而是欺骗硬件,并在内存里进行逻辑矫正

核心步骤

  1. 始终请求横向缓冲区:告诉相机 SurfaceTexture 接收的分辨率时,不论你显示是竖是横,宽高一定要设为"大边x小边"。

  2. 强制缓冲区规格:使用相机硬件列表里真实存在的 Landscape 尺寸

    复制代码
     // 创建相机纹理(OES外部纹理)  相机输入流
                    cameraTextureId = TextureProgram.createExternalTexture()
                    cameraTexture = SurfaceTexture(cameraTextureId).apply {
                        // 正常手机是横向的必须要 横向分辨率,也是输出画面变形拉伸的根本原因
                        if (itsaWidescreen){
                            if ((displayRotationDegrees==0 || displayRotationDegrees==180)){
                                setDefaultBufferSize(height, width)
                            }else{
                                setDefaultBufferSize(width, height)
                            }
                        }else{
                            // 记录仪
                            if (displayRotationDegrees==0 || displayRotationDegrees==180){
                                setDefaultBufferSize(width, height)
                            }else{
                                setDefaultBufferSize(height, width)
                            }
                        }
                        setOnFrameAvailableListener({ onFrameAvailable() }, glHandler)
                    }
                    cameraSurface = Surface(cameraTexture!!)
    
                    // 创建OSD纹理生成器
                    osdGenerator = OsdTextureGenerator(width, height)
                    osdGenerator!!.initialize()
    
                    // 创建拍照用的FBO
                    setupCaptureFbo(width, height)

经验总结

在处理相机相关的拉伸、旋转问题时,可以总结为以下口诀:

  • 硬件请求,始终横向:setDefaultBufferSize 永远是大数在前,小数在后。

  • 坐标旋转,丢给矩阵:真正想要竖屏预览,利用 OpenGL 的 Matrix.rotateM 旋转 combinedMvpMatrix。

  • 观察日志,尊重元数据:characteristics 报出来的 Supported Sizes 如果全都是长大于宽,那千万不要试图通过设置去强行逆天而行。

通过这个简单的尺寸掉转,原本拉伸的 4:3 相机源会按照正确的物理尺寸填充,轻微拉伸现象立刻消失!

相关推荐
seabirdssss3 小时前
Appium 在小米平板上的安装受限与闪退排查
android·appium·电脑
喂_balabala3 小时前
Kotlin-属性委托
android·开发语言·kotlin
空中海3 小时前
第一章:Android 系统架构与核心原理
android·系统架构
lI-_-Il3 小时前
适配工具箱:手机里的全能数字瑞士军刀
android·音视频
彳亍走的猪3 小时前
Android 全局防抖/防重复点击
android·java·开发语言
程序员陆业聪4 小时前
Android图片加载框架深度对比:Coil 3.4.0 vs Glide 5.0,该选哪个?
android
seabirdssss4 小时前
Android 模拟器搭建
android·经验分享
CYRUS STUDIO4 小时前
Frida 源码编译全流程:自己动手编译 frida-server
android·安全·逆向
程序员陆业聪4 小时前
Android内存优化:当LeakCanary遇上协程,内存泄漏治理进入新阶段
android