使用Android L中提供的MediaProjection
API,可以
将主屏幕的内容(默认显示)捕获到Surface对象中,然后您的应用可以通过网络发送该对象
我设法让VirtualDisplay
正常工作,我的SurfaceView
正确显示了屏幕内容。
我想要做的是捕捉Surface
中显示的框架,然后将其打印到文件中。我尝试了以下内容,但我得到的只是一个黑色文件:
Bitmap bitmap = Bitmap.createBitmap
(surfaceView.getWidth(), surfaceView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
surfaceView.draw(canvas);
printBitmapToFile(bitmap);
如何从Surface
?
因为@j__m建议我现在使用VirtualDisplay
的{{1}}设置Surface
:
ImageReader
然后我创建了将Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
displayWidth = size.x;
displayHeight = size.y;
imageReader = ImageReader.newInstance(displayWidth, displayHeight, ImageFormat.JPEG, 5);
传递给Surface
:
MediaProjection
最后,为了得到一个"截图"我从int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
DisplayMetrics metrics = getResources().getDisplayMetrics();
int density = metrics.densityDpi;
mediaProjection.createVirtualDisplay("test", displayWidth, displayHeight, density, flags,
imageReader.getSurface(), null, projectionHandler);
获取Image
并从中读取数据:
ImageReader
问题是生成的位图为Image image = imageReader.acquireLatestImage();
byte[] data = getDataFromImage(image);
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
。
这是null
方法:
getDataFromImage
public static byte[] getDataFromImage(Image image) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.capacity()];
buffer.get(data);
return data;
}
返回的Image
始终为默认大小为7672320的数据,解码返回acquireLatestImage
。
更具体地说,当null
尝试获取图片时,会返回状态ImageReader
。
答案 0 :(得分:11)
在花了一些时间学习Android图形架构之后,我得到了它的工作。所有必要的部分都有详细记录,但如果您还不熟悉OpenGL,可能会引起麻烦,所以这里有一个很好的总结"对于假人而言#34;。
我假设你
BufferQueue是ImageReader
的意思。这个课程的名字很难开头 - 最好把它叫做#34; ImageReceiver" - 一个接收BufferQueue端的哑包装器(通过任何其他公共API无法访问)。不要被愚弄:它不会进行任何转换。即使C ++ BufferQueue在内部公开该信息,它也不允许生成器支持的查询格式。它可能在简单的情况下失败,例如,如果生产者使用自定义的,模糊的格式(例如BGRA)。
上面列出的问题是我建议使用OpenGL ES glReadPixels 作为通用回退的原因,但仍然尝试使用ImageReader(如果可用),因为它可能允许使用最少的副本/转换来检索图像。
为了更好地了解如何使用OpenGL完成任务,让我们看一下ImageReader / MediaCodec返回的Surface
。没有什么特别的,只有SurfaceTexture顶部的普通Surface有两个陷阱:OES_EGL_image_external
和EGL_ANDROID_recordable
。
简单地说,OES_EGL_image_external是一个flag,必须传递给 glBindTexture 才能使纹理与BufferQueue一起工作。它不是定义特定的颜色格式等,而是从生产者那里收到的任何不透明的容器。实际内容可以是YUV颜色空间(对于Camera API是强制性的),RGBA / BGRA(通常由视频驱动程序使用)或其他可能的特定于供应商的格式。制作人可能会提供一些细节,例如JPEG或RGB565表示,但不要把你的希望寄托在高处。
从Android 6.0开始,CTS测试覆盖的唯一一个制作人是Camera API(仅AFAIK的Java外观)。原因是,为什么有很多MediaProjection + RGBA8888 ImageReader示例飞来飞去是因为它是一个经常遇到的共同面额和唯一的格式,由glReadPixels的OpenGL ES规范强制要求。如果显示作曲家决定使用完全不可读的格式或者仅仅是ImageReader类不支持的格式(例如BGRA8888),你仍然不会感到惊讶,你将不得不处理它。
从阅读the specification可以看出,它是一个标志,传递给 eglChooseConfig ,以便轻轻地推动制作人生成YUV图像。或优化管道以便从视频内存中读取。或者其他的东西。我不知道任何CTS测试,确保它的正确处理(甚至规范本身表明,个别生产者可能会被硬编码以给予特殊处理),所以如果发生这种情况不要感到惊讶不受支持(请参阅Android 5.0模拟器)或默默忽略。 Java类中没有定义,只需自己定义常量,就像Grafika那样。
那么应该怎样做才能从背景中读取VirtualDisplay"正确的方式"?
您的OnFrameAvailableListener
回调将包含以下步骤:
使用ImageReader读取视频内存时,内部执行了大多数步骤,但有些不同。创建缓冲区中行的对齐可以通过glPixelStore定义(默认为4,因此在使用4字节RGBA8888时不必考虑它。)
请注意,除了使用着色器处理纹理外,GL ES不会在格式之间自动转换(与桌面OpenGL不同)。如果你想要RGBA8888数据,请确保以该格式分配屏幕外缓冲区并从glReadPixels请求它。
EglCore eglCore;
Surface producerSide;
SurfaceTexture texture;
int textureId;
OffscreenSurface consumerSide;
ByteBuffer buf;
Texture2dProgram shader;
FullFrameRect screen;
...
// dimensions of the Display, or whatever you wanted to read from
int w, h = ...
// feel free to try FLAG_RECORDABLE if you want
eglCore = new EglCore(null, EglCore.FLAG_TRY_GLES3);
consumerSide = new OffscreenSurface(eglCore, w, h);
consumerSide.makeCurrent();
shader = new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)
screen = new FullFrameRect(shader);
texture = new SurfaceTexture(textureId = screen.createTextureObject(), false);
texture.setDefaultBufferSize(reqWidth, reqHeight);
producerSide = new Surface(texture);
texture.setOnFrameAvailableListener(this);
buf = ByteBuffer.allocateDirect(w * h * 4);
buf.order(ByteOrder.nativeOrder());
currentBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
只有在完成上述所有操作后,您才能使用producerSide
Surface初始化VirtualDisplay。
帧回调代码:
float[] matrix = new float[16];
boolean closed;
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// there may still be pending callbacks after shutting down EGL
if (closed) return;
consumerSide.makeCurrent();
texture.updateTexImage();
texture.getTransformMatrix(matrix);
consumerSide.makeCurrent();
// draw the image to framebuffer object
screen.drawFrame(textureId, matrix);
consumerSide.swapBuffers();
buffer.rewind();
GLES20.glReadPixels(0, 0, w, h, GLES10.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
buffer.rewind();
currentBitmap.copyPixelsFromBuffer(buffer);
// congrats, you should have your image in the Bitmap
// you can release the resources or continue to obtain
// frames for whatever poor-man's video recorder you are writing
}
上面的代码是this Github project中大大简化的方法版本,但所有引用的类都直接来自Grafika。
根据您的硬件,您可能需要跳出一些额外的箍来完成任务:使用 setSwapInterval ,在制作屏幕截图之前调用 glFlush 等。其中大部分可以是从LogCat的内容中自行想出来。
为了避免Y坐标反转,请将Grafika使用的顶点着色器替换为以下一个:
String VERTEX_SHADER_FLIPPED =
"uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uTexMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vec2 coordInterm = (uTexMatrix * aTextureCoord).xy;\n" +
// "OpenGL ES: how flip the Y-coordinate: 6542nd edition"
" vTextureCoord = vec2(coordInterm.x, 1.0 - coordInterm.y);\n" +
"}\n";
当ImageReader不适合您时,或者您想在从GPU移动图像之前对Surface内容执行一些着色器处理时,可以使用上述方法。
通过对屏幕外缓冲区执行额外复制可能会损害其速度,但如果您知道接收缓冲区的确切格式(例如来自ImageReader)并使用相同格式的glReadPixels,则运行着色器的影响将会很小
例如,如果您的视频驱动程序使用BGRA作为内部格式,您将检查是否支持EXT_texture_format_BGRA8888
(可能会),分配屏幕外缓冲区并使用glReadPixels以此格式检索图像。
如果您想要执行完整的零拷贝或使用OpenGL不支持的格式(例如JPEG),您最好还是使用ImageReader。
答案 1 :(得分:6)
各种“我如何捕获SurfaceView的屏幕截图”答案(e.g. this one)仍然适用:你不能这样做。
SurfaceView的曲面是一个单独的图层,由系统合成,独立于基于视图的UI图层。曲面不是像素的缓冲区,而是缓冲区的队列,具有生产者 - 消费者安排。你的应用程序是在制作人一方。获得屏幕截图需要您在消费者方面。
如果将输出定向到SurfaceTexture而不是SurfaceView,则应用程序进程中将包含缓冲区队列的两侧。您可以使用GLES渲染输出并将其读入具有glReadPixels()
的数组。 Grafika有一些使用相机预览做这样的事情的例子。
要将屏幕捕获为视频,或通过网络发送,您可能希望将其发送到MediaCodec编码器的输入表面。
有关Android图形架构的更多详细信息,请here。
答案 2 :(得分:5)
ImageReader
是您想要的课程。
https://developer.android.com/reference/android/media/ImageReader.html
答案 3 :(得分:5)
我有这个工作代码:
mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);
mProjection.createVirtualDisplay("test", width, height, density, flags, mImageReader.getSurface(), new VirtualDisplayCallback(), mHandler);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = null;
FileOutputStream fos = null;
Bitmap bitmap = null;
try {
image = mImageReader.acquireLatestImage();
fos = new FileOutputStream(getFilesDir() + "/myscreen.jpg");
final Image.Plane[] planes = image.getPlanes();
final Buffer buffer = planes[0].getBuffer().rewind();
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap.compress(CompressFormat.JPEG, 100, fos);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos!=null) {
try {
fos.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (bitmap!=null)
bitmap.recycle();
if (image!=null)
image.close();
}
}
}, mHandler);
我相信Bytebuffer上的倒带()做了伎俩,但不确定为什么。我正在针对Android模拟器21进行测试,因为目前我还没有安装Android-5.0设备。
希望它有所帮助!
答案 4 :(得分:0)
我有以下工作代码:-适用于平板电脑和移动设备:-
private void createVirtualDisplay() {
// get width and height
Point size = new Point();
mDisplay.getSize(size);
mWidth = size.x;
mHeight = size.y;
// start capture reader
if (Util.isTablet(getApplicationContext())) {
mImageReader = ImageReader.newInstance(metrics.widthPixels, metrics.heightPixels, PixelFormat.RGBA_8888, 2);
}else{
mImageReader = ImageReader.newInstance(mWidth, mHeight, PixelFormat.RGBA_8888, 2);
}
// mImageReader = ImageReader.newInstance(450, 450, PixelFormat.RGBA_8888, 2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mVirtualDisplay = sMediaProjection.createVirtualDisplay(SCREENCAP_NAME, mWidth, mHeight, mDensity, VIRTUAL_DISPLAY_FLAGS, mImageReader.getSurface(), null, mHandler);
}
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
int onImageCount = 0;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onImageAvailable(ImageReader reader) {
Image image = null;
FileOutputStream fos = null;
Bitmap bitmap = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
image = reader.acquireLatestImage();
}
if (image != null) {
Image.Plane[] planes = new Image.Plane[0];
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
planes = image.getPlanes();
}
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mWidth;
// create bitmap
//
if (Util.isTablet(getApplicationContext())) {
bitmap = Bitmap.createBitmap(metrics.widthPixels, metrics.heightPixels, Bitmap.Config.ARGB_8888);
}else{
bitmap = Bitmap.createBitmap(mWidth + rowPadding / pixelStride, mHeight, Bitmap.Config.ARGB_8888);
}
// bitmap = Bitmap.createBitmap(mImageReader.getWidth() + rowPadding / pixelStride,
// mImageReader.getHeight(), Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
// write bitmap to a file
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy_HH:mm:ss");
String formattedDate = df.format(Calendar.getInstance().getTime()).trim();
String finalDate = formattedDate.replace(":", "-");
String imgName = Util.SERVER_IP + "_" + SPbean.getCurrentImageName(getApplicationContext()) + "_" + finalDate + ".jpg";
String mPath = Util.SCREENSHOT_PATH + imgName;
File imageFile = new File(mPath);
fos = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
IMAGES_PRODUCED++;
SPbean.setScreenshotCount(getApplicationContext(), ((SPbean.getScreenshotCount(getApplicationContext())) + 1));
if (imageFile.exists())
new DbAdapter(LegacyKioskModeActivity.this).insertScreenshotImageDetails(SPbean.getScreenshotTaskid(LegacyKioskModeActivity.this), imgName);
stopProjection();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (bitmap != null) {
bitmap.recycle();
}
if (image != null) {
image.close();
}
}
}
}, mHandler);
}
2> onActivityResult调用:-
if (Util.isTablet(getApplicationContext())) {
metrics = Util.getScreenMetrics(getApplicationContext());
} else {
metrics = getResources().getDisplayMetrics();
}
mDensity = metrics.densityDpi;
mDisplay = getWindowManager().getDefaultDisplay();
3>
public static DisplayMetrics getScreenMetrics(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
display.getMetrics(dm);
return dm;
}
public static boolean isTablet(Context context) {
boolean xlarge = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == 4);
boolean large = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE);
return (xlarge || large);
}
希望这可以帮助谁在通过MediaProjection Api捕获时在设备上获取失真的图像。
答案 5 :(得分:-4)
我会做这样的事情:
首先,在SurfaceView
实例
surfaceView.setDrawingCacheEnabled(true);
将位图加载到surfaceView
然后,在printBitmapToFile()
:
Bitmap surfaceViewDrawingCache = surfaceView.getDrawingCache();
FileOutputStream fos = new FileOutputStream("/path/to/target/file");
surfaceViewDrawingCache.compress(Bitmap.CompressFormat.PNG, 100, fos);
不要忘记关闭流。此外,对于PNG格式,将忽略质量参数。