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 图像。
这看起来很容易做到,但实际上我很难找到解决方案。
注意:为了使解释更简单并使代码片段更易于理解,我使用具有上述分辨率的硬编码数字,您的代码实际上设置为具有不同的分辨率。
我尝试过的事情:
- 使用 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,而是它可以达到的最大尺寸(屏幕尺寸),所以我最终得到了和以前一样的结果。
- 使用 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),它可能会有更好的结果,但仍然不是一个好的方法,因为我希望它适用于不同的分辨率。
- 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 做同样的事情,这将是向前迈出的一步,因为那样我只需要将图像居中,而视频的边距将被忽略。
- 使用 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() 方法调整视频大小。
我遇到的问题如下:
我有一个自定义板,屏幕分辨率为 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 图像。
这看起来很容易做到,但实际上我很难找到解决方案。
注意:为了使解释更简单并使代码片段更易于理解,我使用具有上述分辨率的硬编码数字,您的代码实际上设置为具有不同的分辨率。
我尝试过的事情:
- 使用 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,而是它可以达到的最大尺寸(屏幕尺寸),所以我最终得到了和以前一样的结果。
- 使用 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),它可能会有更好的结果,但仍然不是一个好的方法,因为我希望它适用于不同的分辨率。
- 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 做同样的事情,这将是向前迈出的一步,因为那样我只需要将图像居中,而视频的边距将被忽略。
- 使用 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() 方法调整视频大小。