如果一个设备屏幕是 16:9 的宽高比,另外一个设备屏幕是 32:9 的宽高比。这种情况下互相共享屏幕,应该怎么处理两个设备之间屏幕宽高比的差异呢?
裁剪&缩放时机
什么时候是最合适裁剪时机呢?
我们思考有几个可能的地方:
- sdp 会交换双方设备的网络地址与屏幕分辨率尺寸等信息
- 采集端采集视频到传输之间
- 渲染端接收到视频帧到渲染到 surfaceview 之间
[Android WebRTC VideoFrame] 一文中提及到在采集时候做美颜和滤镜的处理,官方既然已经提供了在视频帧传输前的 hook 处理时机,这个时机同样适用于裁剪和缩放,我们认为这个时机就是最佳时机。
VideoProcessor
/**
* Applies the frame adaptation parameters to a frame. Returns null if the frame is meant to be
* dropped. Returns a new frame. The caller is responsible for releasing the returned frame.
*/
public static @Nullable VideoFrame applyFrameAdaptationParameters(
VideoFrame frame, FrameAdaptationParameters parameters) {
if (parameters.drop) {
return null;
}
final VideoFrame.Buffer adaptedBuffer =
frame.getBuffer().cropAndScale(parameters.cropX, parameters.cropY, parameters.cropWidth,
parameters.cropHeight, parameters.scaleWidth, parameters.scaleHeight);
return new VideoFrame(adaptedBuffer, frame.getRotation(), parameters.timestampNs);
}
VideoFrame & cropAndScale
VideoFrame 中提供了这样一个方法
/**
* Crops a region defined by `cropx`, `cropY`, `cropWidth` and `cropHeight`. Scales it to size
* `scaleWidth` x `scaleHeight`.
*/
@CalledByNative("Buffer")
Buffer cropAndScale(
int cropX, int cropY, int cropWidth, int cropHeight, int scaleWidth, int scaleHeight);
做下各个参数的释义:
参数说明
- cropX: 裁剪区域的左上角的 X 坐标。
- cropY: 裁剪区域的左上角的 Y 坐标。
- cropWidth: 裁剪区域的宽度。
- cropHeight: 裁剪区域的高度。
- scaleWidth: 裁剪后图像的目标宽度。
- scaleHeight: 裁剪后图像的目标高度。
详细含义
-
裁剪 (Crop) :
- 通过指定
(cropX, cropY)
确定裁剪区域的左上角。 cropWidth
和cropHeight
决定了裁剪区域的大小。- 这个过程会从原始图像中提取一个子矩形区域。
- 通过指定
-
缩放 (Scale) :
- 在裁剪区域确定后,将该区域缩放到指定的
scaleWidth
和scaleHeight
。 - 缩放操作可以是放大也可以是缩小,具体取决于裁剪区域和目标尺寸的相对大小。
- 在裁剪区域确定后,将该区域缩放到指定的
实践
时机有了,方法也有了,接下来我们来实践一下。 先取两块一样大小的 pad 都做横屏 设备宽高是:Physical size: 1876x3000
为什么打印出来的是 1080 * 1726
FrameAdaptationParameters{cropX=0, cropY=0, cropWidth=1726, cropHeight=1080, scaleWidth=1726, scaleHeight=1080, timestampNs=27612737112000, drop=false}
1080 / 1726 = 0.625 1876 / 3000 = 0.625
宽高比是一样的
每个方法都是本人亲自试验了的,直接提供一个轮子:
public class CropOrScaleUtils {
/**
* 裁剪自身中间一半,不缩放. 视觉中心*2
*
* @param parameters
*/
public static void halfMiddle(VideoProcessor.FrameAdaptationParameters parameters) {
if (parameters == null) {
return;
}
parameters.cropX = parameters.cropWidth >> 2;
parameters.cropY = parameters.cropHeight >> 2;
parameters.cropWidth >>= 1;
parameters.cropHeight >>= 1;
}
/**
* 裁剪自身上半部分,高度缩小2 . 视觉上部
*
* @param parameters
*/
public static void halfTop(VideoProcessor.FrameAdaptationParameters parameters) {
if (parameters == null) {
return;
}
parameters.cropHeight >>= 1;
parameters.scaleHeight >>= 1;//高度必须缩小 2 不然会拉伸
}
/**
* 裁剪自身下半部分,高度缩小2 . 视觉下部
*
* @param parameters
*/
public static void halfBottom(VideoProcessor.FrameAdaptationParameters parameters) {
if (parameters == null) {
return;
}
parameters.cropY = parameters.cropHeight >> 1;
parameters.cropHeight >>= 1;
parameters.scaleHeight >>= 1;//高度必须缩小 2 不然会拉伸
}
/**
* 裁剪自身左半部分,宽度缩小2 . 视觉左部
*
* @param parameters
*/
public static void halfLeft(VideoProcessor.FrameAdaptationParameters parameters) {
if (parameters == null) {
return;
}
parameters.cropWidth >>= 1;
parameters.scaleWidth >>= 1;//宽度必须缩小 2 不然会拉伸
}
/**
* 裁剪自身右半部分,宽度缩小2 . 视觉右部
*
* @param parameters
*/
public static void halfRight(VideoProcessor.FrameAdaptationParameters parameters) {
if (parameters == null) {
return;
}
parameters.cropX = parameters.cropWidth >> 1;
parameters.cropWidth >>= 1;
parameters.scaleWidth >>= 1;//宽度必须缩小 2 不然会拉伸
}
/**
* 模拟同尺寸屏宽高逆转方案1
* 宽高逆转:竖屏向横屏上投或者横屏向竖屏上投
* 暴力缩放会比例失调,但是无黑边填充满屏幕
*/
public static void horizontalAndVerticalReversal1(VideoProcessor.FrameAdaptationParameters parameters) {
int temp = parameters.scaleHeight;
parameters.scaleHeight = parameters.scaleWidth;
parameters.scaleWidth = temp;
}
/**
* 模拟同尺寸屏宽高逆转方案2
* 宽高逆转:竖屏向横屏上投或者横屏向竖屏上投
* 无黑边且保持比例,没有拉伸感,会损失部分画面
*/
public static void horizontalAndVerticalReversal2(VideoProcessor.FrameAdaptationParameters parameters) {
//交换宽高
int temp = parameters.scaleHeight;
parameters.scaleHeight = parameters.scaleWidth;
parameters.scaleWidth = temp;
// 计算裁剪后的宽高比例
float originalAspectRatio = (float) parameters.cropWidth / parameters.cropHeight;
float newAspectRatio = (float) parameters.scaleWidth / parameters.scaleHeight;
if (originalAspectRatio > newAspectRatio) {
// 原始画面更宽,需要裁剪宽度
int newCropWidth = (int) (parameters.cropHeight * newAspectRatio);
int cropWidthDiff = parameters.cropWidth - newCropWidth;
parameters.cropX += cropWidthDiff / 2;
parameters.cropWidth = newCropWidth;
} else {
// 原始画面更高,需要裁剪高度
int newCropHeight = (int) (parameters.cropWidth / newAspectRatio);
int cropHeightDiff = parameters.cropHeight - newCropHeight;
parameters.cropY += cropHeightDiff / 2;
parameters.cropHeight = newCropHeight;
}
// 需要确保裁剪后的尺寸仍然是正数
parameters.cropWidth = Math.max(1, parameters.cropWidth);
parameters.cropHeight = Math.max(1, parameters.cropHeight);
}
/**
* 方法3 什么都不做。既能保持画面完整性,也能保证不被拉伸,只是存在黑边。
*
* @param parameters
*/
public static void horizontalAndVerticalReversal3(VideoProcessor.FrameAdaptationParameters parameters) {
}
}
投屏传输(右投左)+中间裁剪保留原始宽高比游戏画面示例:
总结
设备间传输视频画面最优解是两个设备是两个设备尺寸分辨率一样,或者是设备尺寸不一样宽高比一样也可,但是实际情况往往有很多出入:
webrtc 视频传输如何去处理设备间屏幕或者宽高比差异有以下三种方式
- 保留原输出宽高比,保留所有内容可见,会存在黑边 (默认处理方式)
- 填充满屏幕,保持原输出宽高比,但是会损失部分画面内容
- 填充满屏幕,保留所有内容可见,但是会视觉呈现拉伸效果
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )