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
の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
から返されるacquireLatestImage
には、常にデフォルトサイズが7672320のデータがあり、デコードするとnull
が返されます。
より具体的には、ImageReader
がイメージを取得しようとすると、ステータスACQUIRE_NO_BUFS
が返されます。
時間をかけてAndroidグラフィックアーキテクチャについて少し望ましいことを学んだ後、私はそれを動作させることができました。必要なすべての部分は十分に文書化されていますが、OpenGLに慣れていない場合は頭痛の種になる可能性があるため、ここに「ダミー用」のいい要約があります。
私はあなたが
BufferQueueとは、ImageReader
のことです。そのクラスの名前は、最初は不十分でした。「ImageReceiver」と呼んだ方がいいでしょう。これは、BufferQueue(他のパブリックAPIからはアクセスできません)の受信側の周りのダムラッパーです。だまされないでください。変換は実行されません。 C++ BufferQueueがその情報を内部で公開している場合でも、プロデューサーがサポートするクエリ形式は許可されません。たとえば、プロデューサーがカスタムのあいまいな形式(BGRAなど)を使用している場合など、単純な状況では失敗する可能性があります。
上記の問題は、OpenGL ESglReadPixelsを一般的なフォールバックとして使用することをお勧めしますが、可能であればImageReaderを使用しようとします。これにより、最小限のコピーでイメージを取得できる可能性があるためです/変換。
OpenGLをタスクに使用する方法をよりよく理解するために、ImageReader/MediaCodecによって返されるSurface
を見てみましょう。これは特別なことではなく、OES_EGL_image_external
とEGL_Android_recordable
の2つの問題があるSurfaceTexture上の通常のSurfaceです。
簡単に言えば、 OES_EGL_image_external は flag であり、テクスチャをBufferQueueで機能させるためにglBindTextureに渡す必要があります。特定の色形式などを定義するのではなく、プロデューサーから受け取ったものをすべて不透明なコンテナーにします。実際のコンテンツは、YUV色空間(Camera APIでは必須)、RGBA/BGRA(ビデオドライバーでよく使用される)、またはベンダー固有の形式である可能性があります。プロデューサーは、JPEGやRGB565表現など、いくつかの優れた機能を提供している可能性がありますが、期待は高くありません。
Android 6.0の時点でCTSテストの対象となっている唯一のプロデューサーはCamera APIです(AFAIKはJavaファサードのみです)。多くのMediaProjection + RGBA8888 ImageReaderの例が飛んでいるのは、これが頻繁に発生する一般的な名称であり、glReadPixelsのOpenGL ES仕様で義務付けられている唯一の形式だからです。それでも、display composerが完全に読み取り不可能な形式、またはImageReaderクラス(BGRA8888など)でサポートされていない形式を使用することに決めても、それに対処しなければならない場合でも驚かないでください。
仕様 を読むことから明らかなように、これはフラグであり、プロデューサーをYUVイメージの生成に向けて穏やかにプッシュするためにeglChooseConfigに渡されます。または、ビデオメモリから読み取るためにパイプラインを最適化します。か何か。私はCTSテストを認識していませんが、それが正しい処理であることを確認しています(仕様自体でさえ、個々のプロデューサーが特別な処理を行うようにハードコードされている可能性があることを示唆しています)。Android 5.0エミュレーター)または黙って無視されます。 Javaクラスには定義がありません。Grafikaが行うように、定数を自分で定義するだけです。
では、VirtualDisplayからバックグラウンドで「正しい方法」で読み取るにはどうすればよいでしょうか。
OnFrameAvailableListener
コールバックには、次の手順が含まれます。
上記のステップのほとんどは、ImageReaderでビデオメモリを読み取るときに内部的に実行されますが、一部は異なります。作成されたバッファの行の配置は、glPixelStoreで定義できます(デフォルトは4なので、4バイトRGBA8888を使用する場合は、それを考慮する必要はありません)。
シェーダーGL ESによるテクスチャの処理を除いて、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
}
上記のコードは このGithubプロジェクト にあるアプローチの大幅に簡略化されたバージョンですが、参照されるすべてのクラスは Grafika から直接取得されます。
ハードウェアによっては、処理を完了するためにいくつかの追加のフープをジャンプする必要がある場合があります:setSwapIntervalを使用し、スクリーンショットを作成する前にglFlushを呼び出しますこれらのほとんどは、LogCatのコンテンツから自分で把握できます。
Y座標の反転を回避するには、Grfikaで使用される頂点シェーダーを次のシェーダーに置き換えます。
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を使用するほうがよいでしょう。
さまざまな「SurfaceViewのスクリーンショットをキャプチャするにはどうすればよいですか」という答え( たとえば、これ )がすべて当てはまります。それはできません。
SurfaceViewのサーフェスは、ビューベースのUIレイヤーとは独立した、システムによって合成された別個のレイヤーです。サーフェスは、ピクセルのバッファーではなく、バッファーのキューであり、プロデューサー/コンシューマーの配置を備えています。アプリはプロデューサー側にあります。スクリーンショットを取得するには、消費者側にいる必要があります。
出力をSurfaceViewではなく、SurfaceTextureに送信する場合、アプリプロセスにバッファキューの両側が存在します。 GLESを使用して出力をレンダリングし、glReadPixels()
を使用して配列に読み込むことができます。 Grafika には、カメラプレビューでこのようなことを行う例がいくつかあります。
画面をビデオとしてキャプチャしたり、ネットワークを介して送信したりするには、MediaCodecエンコーダーの入力サーフェスに送信します。
Androidグラフィックスアーキテクチャの詳細は利用可能です こちら 。
私はこの作業コードを持っています:
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のrewind()がうまくいったと信じていますが、その理由はよくわかりません。現在、Android = 5.0デバイスを手元に置いていないため、Androidエミュレータ21に対してテストしています。
それが役に立てば幸い!
ImageReader
は必要なクラスです。
https://developer.Android.com/reference/Android/media/ImageReader.html
私はこの作業コードを持っています:-タブレットとモバイルデバイス用:-
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を介してキャプチャしているときに、デバイスで歪んだ画像を取得している人に役立つことを願っています。