Android: 在 SurfaceView 中从 MediaCodec 裁剪视频

Android: Crop video from MediaCodec in SurfaceView

我遇到的问题如下:

我有一个自定义板,屏幕分辨率为 900x500 像素。该板运行 Android 8.0.

我得到一个 1280x720 分辨率的视频流(来自 android auto,当手机连接到开发板时),但我的屏幕尺寸为 900x500 的视频中包含内容。

基本上,如果您看到完整的视频,就会在较大的帧中看到较小的图像,在 (1280-900)/2 = 190 和 (720-500)/2 = 110 的一侧有黑边,例如下图:

Video frame image example

(我有一张图片要放在这里,但由于这是我的第一个 post 我不被允许,我将用 ascii 绘制它。) (编辑:意识到为附加图像创建了 link,但我会保留 ascii 以防万一))

                                    1280
****************************************************************************
*FULL VIDEO                           |                                    *
*                                    110                                   *
*                                     |                                    *
*            **************************************************            *
*            *                       900                      *            *
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*----190-----*                     CONTENT                500 *----190-----* 720
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*            *                                                *            *
*            **************************************************            *
*                                     |                                    *
*                                    110                                   *
*                                     |                                    *
****************************************************************************

我使用 MediaCodec 解码视频流,使用 SurfaceView 将视频渲染到屏幕上(还需要触摸事件)。

使用此设置,整个 1280x720 视频将缩小到 900x500 屏幕,因此可以在屏幕上看到全部内容(即,整个 1280x720 内容将缩小到 900x500)。我实际需要的是从侧面裁剪黑边并仅使用内部的 900x500 图像。

这看起来很容易做到,但实际上我很难找到解决方案。

注意:为了使解释更简单并使代码片段更易于理解,我使用具有上述分辨率的硬编码数字,您的代码实际上设置为具有不同的分辨率。

我尝试过的事情:

  1. 使用 SurfaceView 方法 setLayoutParams 并向其传递带边距的布局:

代码片段

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mSurfaceView.getLayoutParams();
layoutParams.width = 900;
layoutParams.height = 500;
layoutParams.setMargins(190, 110, 190, 110);
mSurfaceView.setLayoutParams(layoutParams);
mSurfaceView.requestLayout();

这当然行不通。布局参数属于 surfaceView 本身,因此在调用 setMargins 时,它们会应用于 surfaceView,使其更小:900 - 190*2 = 520 和 500 - 110*2 = 280。最后我得到了 520x280 的表面(甚至没有使用孔屏)并且整个视频都缩小到那个尺寸。所以最后没有裁剪视频本身的边距。

我在这里的想法是设置宽度和高度分别为 1280 和 720 的表面视图(比屏幕本身磨碎的 surfaceView),然后使用 setMargins 将 surfaceView 裁剪为屏幕的大小。这基本上就是我需要的。

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mSurfaceView.getLayoutParams();
layoutParams.width = 1280;
layoutParams.height = 720;
layoutParams.setMargins(190, 110, 190, 110);
mSurfaceView.setLayoutParams(layoutParams);
mSurfaceView.requestLayout();

这也不起作用,因为宽度和高度大于屏幕尺寸,Android 不会使其达到 1280x720,而是它可以达到的最大尺寸(屏幕尺寸),所以我最终得到了和以前一样的结果。

  1. 使用 SurfaceView 方法 setScaleX 和 setScaleY

就像我提到的那样,视频在渲染到表面时会自动缩小,所以我试图做的是将其放大。宽度从 1280 缩放到 900 以适合表面,因此我将其缩放回 1280(即,将其宽度缩放 1280/900 = 1.42222...)。身高也一样。

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mSurfaceView.getLayoutParams();
layoutParams.width = 900;
layoutParams.height = 500;
mSurfaceView.setLayoutParams(layoutParams);
mSurfaceView.setScaleX(1.44f);
mSurfaceView.setScaleY(1.42f);
mSurfaceView.requestLayout();

这果然是最接近我想要的方法。然而,视频变得一团糟,尤其是它的颜色。我认为主要原因是例如 1280/900 有无限小数,所以这种缩放比例似乎不太好。

另外,先缩小然后再放大的方法从一开始听起来并不好,因为信息可能会丢失。如果缩放可以用更好的数字完成(例如从 1000 缩放到 500 然后回到 1000),它可能会有更好的结果,但仍然不是一个好的方法,因为我希望它适用于不同的分辨率。

  1. MediaCodec 缩放以适应和缩放以适应裁剪

在 MediaCodec 方面,我尝试了两种缩放模式:

mCodec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);

and

mCodec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);

VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING 的声音听起来很有希望,但这仅意味着视频将被裁剪以适合整个表面但保持其纵横比(因此内容不会失真)。

好像MediaCodec中如果有另一个选项就好了VIDEO_SCALING_MODE_NO_SCALING,但是没有...只有上面两个选项

注意:在测试时,我制作了一个简单的应用程序来更轻松地进行测试,我将在其中使用 surfaceView 但我没有使用视频,而是使用了 1280x720 的图像并使用 SurfaceView.draw 方法绘制了它。这产生了不同的结果,因为图像没有缩放,所以只绘制适合屏幕的内容(前 900 个像素和前 500 个像素)。如果我可以对 MediaCodec 做同样的事情,这将是向前迈出的一步,因为那样我只需要将图像居中,而视频的边距将被忽略。

  1. 使用 MediaCodec 输入缓冲区。

我尝试的另一件事是修改 MediaCodec 解码的数据缓冲区中的视频。 想法是获取 inputBuffer,将其转换为位图,移除位图的边距,然后将位图转换回字节缓冲区。

在将 inputBuffer 转换为位图时,我在实施此方法时遇到了一些问题。我开始阅读它以及媒体编解码器如何使用 YUV 而不是 ARGB,但这取决于 YUV 类型的来源。

在我得到任何结果之前我最终放弃了这个想法,主要是因为你似乎需要知道你正在使用什么类型的 YUV,如果它改变了解决方案将不起作用。此外,这似乎不会有最佳性能,因为您将对每一帧执行 ByteBuffer -> Bitmap -> ByteBuffer 的过程。

无论如何,我尝试了更多的东西,但我不想让它比现在更长。我尝试的大多数事情都与我提到的前两件事更相关:使用 SurfaceView 工作。

知道如何解决这个问题吗?

注意:我可以进行任何必要的更改,例如将 SurfaceView 更改为 TextureView。此外,我正在使用自定义板并完全控制 AOSP,而不仅仅是应用程序。事实上,应用程序在 AOSP 中,所以我可以使用任何隐藏方法甚至修改 AOSP。然而,这个问题似乎 none 是必要的,并且可以使用任何常规 SDK 方法在应用程序中解决。

感谢并抱歉提出这么长的问题!这是我的第一个:P

好的,我已经解决了。首先,让我先说,在我在问题中描述的解决方案 2 中(使用 SurfaceView 方法 setScaleX 和 setScaleY)我设置了错误的比例。在我做的宽度示例中:1280/900 = 1.42222... 但后来我将其设置为 setScaleY 而不是 setScaleX。我没有在问题中编辑它,因为即使你只是一个例子,我也确实在实际代码中犯了那个错误。

无论如何,我正确地修正了比例,但我仍然遇到视频被弄乱的问题。然而,当我将状态栏拉下时,图像缩小了一点(可能只有一个像素)并且视频看起来不错,但是状态栏一恢复,视频又搞砸了。我读过 SurfaceView 做了类似的事情,最好使用 TextureView insted。所以基本上看完这个 post:

https://www.hellsoft.se/textureview-transforms-on-android/

我决定换成TextureView,使用setTransform方法。最后代码如下所示:

float scaleX = (float)width/(float)(width - widthMargin);
float scaleY = (float)height/(float)(height - heightMargin);

Matrix matrix = new Matrix();
matrix.setScale(scaleX, scaleY, (float)mScreenWidth/2f, (float)mScreenHeight/2f);

mTextureView.setTransform(matrix);

对于上面的示例,这将是:

float scaleX = 1280f/1280f - 380f); //380 = 190*2
float scaleY = 720f/720 - 220f); //220 = 110*2

Matrix matrix = new Matrix();
matrix.setScale(scaleX, scaleY, 900f/2f, 500f/2f);

mTextureView.setTransform(matrix);

基本上与我在 SurfaceView 中使用 setScale 尝试的方法相同,但使用 TextureView 时也是如此。

我在问题中提到我不确定该方法,因为视频正在按比例缩小然后再放大。然而,阅读上面 link 中的 post 让我明白了视频实际上是如何转换以进行渲染的。

TL;DR: 使用 TextureView 和 setTransform() 方法调整视频大小。