14、视频旋转与镜像:旋转角度计算、Matrix旋转、镜像翻转、Camera旋转适配
视频渲染中,旋转和镜像是个绕不开的坑。说实话,我早期做播放器时,就被这个看似简单的问题折腾过好几回。你想想看,明明摄像头拍出来的画面是正的,放到SurfaceView上怎么就倒过来了?或者用户自拍时,镜像效果怎么处理?
这一章,我们就来彻底搞定它。
旋转角度的计算逻辑
先搞清楚一个基本概念:视频的旋转角度,通常来自两个地方。
- 视频文件的旋转信息:MP4等容器里有个metadata叫Rotation,常见值是0、90、180、270。这是拍摄设备写入的。
- 摄像头传感器的朝向:Android设备的前置/后置摄像头,本身就有物理方向。比如后置摄像头通常是0度,前置可能是270度。
我个人的习惯是,先读取MediaMetadataRetriever拿到旋转角度:
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, uri);
String rotation = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
int angle = Integer.parseInt(rotation); // 0, 90, 180, 270
retriever.release();
拿到角度后,我们才能决定怎么旋转。但注意,这个角度是相对于原始录制方向的。如果视频本身是横屏拍的,但播放器是竖屏显示,那你还得考虑显示区域的宽高比。
Matrix旋转:从纹理到屏幕
在OpenGL ES中,旋转是通过Matrix实现的。我们通常操作的是纹理坐标矩阵,而不是顶点坐标矩阵。为什么?因为顶点坐标决定了画面在屏幕上的位置,纹理坐标决定了画面内容怎么映射到顶点上。
旋转的核心代码,我一般这样写:
// 创建纹理坐标矩阵
float[] textureMatrix = new float[16];
Matrix.setIdentityM(textureMatrix, 0);
// 绕Z轴旋转,角度由视频metadata决定
Matrix.rotateM(textureMatrix, 0, angle, 0, 0, 1);
// 应用到纹理坐标
// 在顶点着色器中,用 textureMatrix * textureCoord 即可
这里有个细节:旋转中心是纹理坐标的原点(0,0),也就是左下角。如果你直接旋转,画面会绕着左下角转,而不是中心。所以旋转前需要先平移到中心,旋转完再平移回去。
我习惯封装一个工具方法:
public static float[] getRotationMatrix(int angle, float width, float height) {
float[] matrix = new float[16];
Matrix.setIdentityM(matrix, 0);
// 平移到中心
Matrix.translateM(matrix, 0, 0.5f, 0.5f, 0);
// 旋转
Matrix.rotateM(matrix, 0, angle, 0, 0, 1);
// 平移回原点
Matrix.translateM(matrix, 0, -0.5f, -0.5f, 0);
return matrix;
}
镜像翻转:左右颠倒的秘密
镜像翻转,说白了就是把画面左右对调。前置摄像头自拍时,如果不做镜像,你看到的是别人视角的自己,很别扭。
实现方式很简单:把纹理坐标的x轴取反。
具体做法:
// 镜像矩阵:x轴缩放为-1
float[] mirrorMatrix = new float[16];
Matrix.setIdentityM(mirrorMatrix, 0);
Matrix.scaleM(mirrorMatrix, 0, -1, 1, 1);
// 同样需要平移到中心再平移回来
Matrix.translateM(mirrorMatrix, 0, 0.5f, 0.5f, 0);
Matrix.scaleM(mirrorMatrix, 0, -1, 1, 1);
Matrix.translateM(mirrorMatrix, 0, -0.5f, -0.5f, 0);
为什么是-1?因为纹理坐标x范围是0到1,取反后变成0到-1,画面就左右颠倒了。但这样画面会跑到纹理边界外,所以需要平移回来。
嗯,这里要注意:镜像和旋转的顺序。先旋转再镜像,和先镜像再旋转,结果完全不同。我一般先做旋转,再做镜像。因为旋转是修正视频方向,镜像是修正用户视角,逻辑上分开处理更清晰。
核心原则: 旋转矩阵和镜像矩阵可以合并成一个复合矩阵。但建议分开计算,最后再相乘。这样代码可读性好,也方便调试。
Camera旋转适配:Android设备的噩梦
Android设备的摄像头方向,简直是碎片化的重灾区。不同厂商、不同Android版本,摄像头传感器的朝向都不一样。
我遇到过最坑的一次:某款国产手机,后置摄像头返回的旋转角度是90度,但实际画面是正的。查了半天,发现是厂商在驱动层做了修正,但metadata没更新。
适配Camera旋转,我总结了几个步骤:
- 获取摄像头传感器方向:通过CameraInfo获取。
- 计算预览旋转角度:根据当前屏幕方向,算出预览画面需要旋转多少度。
- 区分前置/后置:前置摄像头需要额外做镜像。
代码示例:
// 获取摄像头传感器方向
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int sensorOrientation = info.orientation; // 通常是90或270
// 计算预览旋转角度
int displayRotation = getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (displayRotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
// 前置摄像头:旋转角度 = (sensorOrientation + degrees) % 360
// 然后镜像翻转
result = (sensorOrientation + degrees) % 360;
// 镜像在纹理矩阵中处理
} else {
// 后置摄像头:旋转角度 = (sensorOrientation - degrees + 360) % 360
result = (sensorOrientation - degrees + 360) % 360;
}
这个公式我记了很久,后来干脆写了个工具类。每次新项目直接复制,省得再推导一遍。
setTargetRotation,导致预览画面和录制画面方向不一致。如果你用Camera2,记得在CaptureRequest.Builder中设置CONTROL_AE_TARGET_FPS_RANGE和JPEG_ORIENTATION。嗯,这个坑我踩了两次才记住。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,从视频源到最终显示,旋转和镜像经历了哪些步骤。
总结一下
视频旋转和镜像,说白了就是矩阵运算。你只要记住三点:
- 角度来源:metadata、传感器、屏幕方向,三者结合才能算出正确角度。
- 矩阵操作:平移→旋转/缩放→平移,这个三步走套路通用所有场景。
- 镜像特殊处理:前置摄像头必须镜像,后置一般不需要。
我在项目中,通常会把旋转和镜像封装成一个统一的TextureTransform类。输入角度和是否镜像,输出一个16位的float矩阵。这样上层调用者只需要关心业务逻辑,不用管底层矩阵怎么算。
嗯,这一章的内容就到这里。代码不多,但背后的原理值得多琢磨。你可以在自己的项目里试试,先打印出各个阶段的矩阵值,看看每一步发生了什么变化。相信我,调试几次后,你对矩阵的理解会上一个台阶。