前言
在进行 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。
真相揭秘
-
硬件层面的 Landscape 特性 :Android 设备的摄像头传感器(Sensor)在硬件物理层面上几乎全都是横向放置的。相机底层驱动能处理的原始流(Output Sizes)绝大多数只支持宽大于高的横向格式(如 1920x1080)。
-
不匹配的回退机制 :当我们向相机请求一个 1080(W) x 1920(H) 的输出流时,Camera2 HAL(硬件抽象层)由于在其支持列表中找不到对应的"竖屏"尺寸,它并不会报错,而是会执行回退策略。
-
4:3 与 16:9 的战争 :通过日志发现,该设备最大支持 4000x3000(4:3比例)。因为驱动识别不了竖着的 16:9,它极大概率会选择传感器全像素尺寸或者一个默认的 4:3 预览尺寸(如 1440x1080)吐出数据。
-
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% 左右的由来。
最终修复
修复这个问题的核心不是去旋转图像,而是欺骗硬件,并在内存里进行逻辑矫正。
核心步骤
-
始终请求横向缓冲区:告诉相机 SurfaceTexture 接收的分辨率时,不论你显示是竖是横,宽高一定要设为"大边x小边"。
-
强制缓冲区规格:使用相机硬件列表里真实存在的 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 相机源会按照正确的物理尺寸填充,轻微拉伸现象立刻消失!