web-dev-qa-db-ja.com

MediaProjectionを使用してスクリーンショットを撮る

Android Lで利用可能なMediaProjection AP​​Iを使用すると、

メイン画面(デフォルトのディスプレイ)のコンテンツを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が示唆したように、VirtualDisplaySurfaceを使用して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);

次に、SurfaceMediaProjectionに渡して仮想ディスプレイを作成します。

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が返されます。

20

時間をかけてAndroidグラフィックアーキテクチャについて少し望ましいことを学んだ後、私はそれを動作させることができました。必要なすべての部分は十分に文書化されていますが、OpenGLに慣れていない場合は頭痛の種になる可能性があるため、ここに「ダミー用」のいい要約があります。

私はあなたが

  • Grafika について知っておきましょう。非公式なAndroidメディアAPIテストスイートであり、仕事を愛するGoogleの従業員が余暇に書いたものです。
  • Khronos GL ES docs を読んで、必要に応じてOpenGL ES知識のギャップを埋めることができます。
  • このドキュメント を読み、そこに書かれたほとんどを理解している(少なくともハードウェアコンポーザーとBufferQueueに関する部分)。

BufferQueueとは、ImageReaderのことです。そのクラスの名前は、最初は不十分でした。「ImageReceiver」と呼んだ方がいいでしょう。これは、BufferQueue(他のパブリックAPIからはアクセスできません)の受信側の周りのダムラッパーです。だまされないでください。変換は実行されません。 C++ BufferQueueがその情報を内部で公開している場合でも、プロデューサーがサポートするクエリ形式は許可されません。たとえば、プロデューサーがカスタムのあいまいな形式(BGRAなど)を使用している場合など、単純な状況では失敗する可能性があります。

上記の問題は、OpenGL ESglReadPixelsを一般的なフォールバックとして使用することをお勧めしますが、可能であればImageReaderを使用しようとします。これにより、最小限のコピーでイメージを取得できる可能性があるためです/変換。


OpenGLをタスクに使用する方法をよりよく理解するために、ImageReader/MediaCodecによって返されるSurfaceを見てみましょう。これは特別なことではなく、OES_EGL_image_externalEGL_Android_recordableの2つの問題があるSurfaceTexture上の通常のSurfaceです。

OES_EGL_image_external

簡単に言えば、 OES_EGL_image_externalflag であり、テクスチャを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など)でサポートされていない形式を使用することに決めても、それに対処しなければならない場合でも驚かないでください。

EGL_Android_recordable

仕様 を読むことから明らかなように、これはフラグであり、プロデューサーをYUVイメージの生成に向けて穏やかにプッシュするためにeglChooseConfigに渡されます。または、ビデオメモリから読み取るためにパイプラインを最適化します。か何か。私はCTSテストを認識していませんが、それが正しい処理であることを確認しています(仕様自体でさえ、個々のプロデューサーが特別な処理を行うようにハードコードされている可能性があることを示唆しています)。Android 5.0エミュレーター)または黙って無視されます。 Javaクラスには定義がありません。Grafikaが行うように、定数を自分で定義するだけです。

難しい部分への行き方

では、VirtualDisplayからバックグラウンドで「正しい方法」で読み取るにはどうすればよいでしょうか。

  1. EGLコンテキストとEGL表示を作成します。「記録可能」フラグを使用することもできますが、必ずしもそうする必要はありません。
  2. ビデオメモリから読み取られる前に、画像データを格納するためのオフスクリーンバッファーを作成します。
  3. GL_TEXTURE_EXTERNAL_OESテクスチャを作成します。
  4. ステップ3のテクスチャをステップ2のバッファに描画するためのGLシェーダを作成します。ビデオドライバは、(できれば)「外部」テクスチャに含まれるすべてのものが従来のRGBAに安全に変換されるようにします(参照)仕様)。
  5. 「外部」テクスチャを使用して、Surface + SurfaceTextureを作成します。
  6. OnFrameAvailableListenerを上記のSurfaceTextureにインストールします(これは次のステップの前に実行する必要があります。そうしないと、BufferQueueがねじ込まれます!)
  7. ステップ5のサーフェスをVirtualDisplayに提供します。

OnFrameAvailableListenerコールバックには、次の手順が含まれます。

  • コンテキストを最新にします(オフスクリーンバッファーを最新にするなど)。
  • updateTexImage:プロデューサーから画像をリクエストします。
  • getTransformMatrixを使用して、テクスチャの変換マトリックスを取得し、プロデューサーの出力を悩ませている狂気を修正します。このマトリックスはOpenGLの上下逆の座標系を修正しますが、次のステップで上下を逆にすることに注意してください。
  • 以前に作成したシェーダーを使用して、オフスクリーンバッファーに「外部」テクスチャを描画します。最終的に画像が反転したくない場合を除き、シェーダーはY座標をさらに反転する必要があります。
  • GlReadPixelsを使用して、オフスクリーンビデオバッファーからByteBufferに読み取ります。

上記のステップのほとんどは、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を使用するほうがよいでしょう。

13
user1643723

さまざまな「SurfaceViewのスクリーンショットをキャプチャするにはどうすればよいですか」という答え( たとえば、これ )がすべて当てはまります。それはできません。

SurfaceViewのサーフェスは、ビューベースのUIレイヤーとは独立した、システムによって合成された別個のレイヤーです。サーフェスは、ピクセルのバッファーではなく、バッファーのキューであり、プロデューサー/コンシューマーの配置を備えています。アプリはプロデューサー側にあります。スクリーンショットを取得するには、消費者側にいる必要があります。

出力をSurfaceViewではなく、SurfaceTextureに送信する場合、アプリプロセスにバッファキューの両側が存在します。 GLESを使用して出力をレンダリングし、glReadPixels()を使用して配列に読み込むことができます。 Grafika には、カメラプレビューでこのようなことを行う例がいくつかあります。

画面をビデオとしてキャプチャしたり、ネットワークを介して送信したりするには、MediaCodecエンコーダーの入力サーフェスに送信します。

Androidグラフィックスアーキテクチャの詳細は利用可能です こちら

6
fadden

私はこの作業コードを持っています:

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に対してテストしています。

それが役に立てば幸い!

5
mtsahakis
5
j__m

私はこの作業コードを持っています:-タブレットとモバイルデバイス用:-

 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を介してキャプチャしているときに、デバイスで歪んだ画像を取得している人に役立つことを願っています。

0
user3076769