Przechwytywanie ekranu za pomocą MediaProjection

W tym wpisie przedstawiałem, w jaki sposób zrobić zrzuty ekranu aplikacji bez dodatkowych bibliotek. Wspomniałem tam, aby wykonać pełny zrzut ekranu trzeba skorzystać z MediaProjection. W tym artykule właśnie pokaże Ci dokładnie, w jaki sposób to wykonać. Przechwytywanie ekranu za pomocą MediaProjection nie jest zbyt skomplikowane. Za jego pomocą możemy nagrywać obraz, jak i wykonać zdjęcie, zatem zapraszam do lektury 🙂

1. Robienie screenshot’a.

Na początku przyda nam się funkcja, która zmierzy nam wymiary ekranu:

private void getSize(){
    DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
    screenWidth = metrics.widthPixels;
    screenHeight = metrics.heightPixels;
    screenDensity = metrics.densityDpi;
}

Metoda do pobierania obrazu:

@Override
public void onImageAvailable(ImageReader reader) {
    Image image;
    image = reader.acquireLatestImage();
    if (image == null) return;
    final Image.Plane[] planes = image.getPlanes();
    final Buffer buffer = planes[0].getBuffer().rewind();
    int pixelStride = planes[0].getPixelStride();
    int rowStride = planes[0].getRowStride();
    int rowPadding = rowStride - pixelStride * screenWidth;
    // create bitmap
    bitmap = Bitmap.createBitmap(screenWidth+rowPadding/pixelStride, screenHeight, Bitmap.Config.ARGB_8888);
    bitmap.copyPixelsFromBuffer(buffer);
    if (bitmap != null) {
        image.close();
        clean();
    }
}

Główna metoda może wyglądać tak:

public void takescreenshot(){
    mMediaProjectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    if (mMediaProjection != null) {
        try {
            getSize();
            mImageReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, 2);
            mMediaProjection.createVirtualDisplay(
                    "Screenshot",
                    screenWidth,
                    screenHeight,
                    screenDensity,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                    mImageReader.getSurface(),
                    null,
                    null );
            mImageReader.setOnImageAvailableListener( this, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    else {
        Intent intent = mMediaProjectionManager.createScreenCaptureIntent();
        activity.startActivityForResult(intent,REQUEST_CODE_CAPTURE_IMAGE);
    }
}

Na początku musimy wywołać metodę getSystemService z parametrem MEDIA_PROJECTION_SERVICE, która zapewnia uzyskiwania pozwolenia na przechwytywanie obrazu. Dalej mMediaProjectionManager.createScreenCaptureIntent() jest stosowana, aby uruchomić intencji.
MediaProjection permission
Metoda onActivityResult obsługuje wywołania zwrotne i może wyglądać następująco:

public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode != REQUEST_CODE_CAPTURE_IMAGE) {
        Log.e(TAG, "Unknown request code: " + requestCode);
        return;
    }
    if (resultCode != Activity.RESULT_OK) {
        Log.e(TAG, "Screen Cast Permission Denied" );
        return;
    }
    mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
    takescreenshot();
}

Po udostępnieniu mMediaProjection możliwe jest teraz utworzenie wirtualnego ekranu (złożonego ze wszystkich widocznych powierzchni) i wskazanie Androidowi, na którą powierzchnię chcesz skopiować ekran – w naszym przypadku jest to mImageReader.getSurface(). Do przechwytywania ekranu i zapisaniu do obrazu posłuży nam wywowałanie – mImageReader.setOnImageAvailableListener( this, null); Na koniec metoda czyszcząca:

public void clean(){
    if (mMediaProjectionManager != null) {
        mMediaProjectionManager = null;
    }
    if (mMediaProjection != null) {
        mMediaProjection.stop();
    }
    if (mImageReader != null) {
        mImageReader.setOnImageAvailableListener(null, null);
        mImageReader.close();
    }
    if (bitmap != null) {
        bitmap = null;
    }
}

2. Nagrywanie ekranu.

Do nagrywania obrazu skorzystamy z MediaRecorder. Metoda główna zmieni się na coś takiego:

public void startRecord(){
    mMediaProjectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    if (mMediaProjection != null) {
        try {
            getSize();
            mMediaRecorder = new MediaRecorder();
            initRecorder();
            mMediaProjection.createVirtualDisplay(
                    "Recording Screen",
                    screenWidth,
                    screenHeight,
                    screenDensity,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                    mMediaRecorder.getSurface(),
                    null,
                    null);
            mMediaRecorder.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    else {
        Intent intent = mMediaProjectionManager.createScreenCaptureIntent();
        activity.startActivityForResult(intent,REQUEST_CODE_CAPTURE_RECORDING);
    }
}

Kod jest prosty i podobny do wcześniejszego zamiast ImageReader mamy MediaRecoder. Funkcja do zatrzymywania nagrywania może wyglądać tak:

public void stopRecord(){
    if (mMediaRecorder != null) {
        try{
            SystemClock.sleep(500);
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }catch(RuntimeException e){
            e.printStackTrace();
        }
    }

Zastosowałem tutaj celowo SystemClock.sleep(500), ponieważ gdy rozpoczniemy i od razu zatrzymamy nagrywanie, dostaniemy wyjątek. Błąd spowodowany jest tym, że mediarecoder nie otrzymał żadnych informacji o ekranie.
Metoda do zainicjowania mediarecodera:

private void initRecorder() {
    try {
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        mMediaRecorder.setOutputFile(getPath(".mp4"));
        mMediaRecorder.setVideoSize(screenWidth, screenHeight);
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        mMediaRecorder.setVideoEncodingBitRate(512 * 1000);
        mMediaRecorder.setVideoFrameRate(30);
        mMediaRecorder.prepare();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Jeżeli nie chcesz nagrywać dźwięku, możesz usunąć wpisy związane z audio. Dźwięk jest zbierany przez mikrofon, także otoczenie też się zarejestruje,  Ostatnia metoda, która pobiera lokalizację do zapisu pliku wideo:

private String getPath(String format ) {
    String path = Environment.getExternalStorageDirectory() + "/Recorded Screen/";
    File dir = new File(path);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()).format(new Date());
    file = path + date + format;
    return file;
}

3. Podsumowanie.

Krótko i na temat. Powyższy kod można też zmienić i nie koniecznie musisz korzystać z tego rozwiązania, które przedstawiłem. Na przykład, zamiast MediaRecorder można zastosować MediaMuxerMediaCodec, ale uznałem, że powyższy sposób jest prostszy i też skuteczny. Przykładową aplikację można znaleźć tutaj.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂