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 相机源会按照正确的物理尺寸填充,轻微拉伸现象立刻消失!

相关推荐
过期动态16 小时前
MySQL中的约束
android·java·数据库·spring boot·mysql
牛蛙点点申请出战18 小时前
IconFontViewer -- 一个可以在 Android Studio 中实时预览 IconFont 的插件
android·前端·intellij idea
努力努力再努力wz18 小时前
【MySQL 进阶系列】拒绝滥用root:从 mysql.user 到权限校验,带你彻底理解用户管理与授权机制!
android·c语言·开发语言·数据结构·数据库·c++·mysql
HaiXCoder19 小时前
AndroidAutoSize 框架原理分析与核心问题
android
fengci.19 小时前
CTF+随机困难题目
android·开发语言·前端·学习·php
Le_ee20 小时前
SWPUCTF 2025 秋季新生赛wp2
android
pengyu21 小时前
【Kotlin 协程修仙录 · 金丹境 · 初阶】 | 并发艺术:async/await 与并发组合的优雅之道
android·kotlin
沐言人生1 天前
ReactNative 源码分析3——ReactActivity之初始化RN应用
android·react native
YaBingSec1 天前
网络安全靶场WP:Grafana 任意文件读取漏洞(CVE-2021-43798)
android·笔记·安全·web安全·ssh·grafana
YF02111 天前
彻底解决Android非SDK接口绕过限制的深度实践
android·google·app