1. 程式人生 > >Android Multimedia實戰(四)MediaProjection實現截圖,與MediaMuxer實現錄屏為MP4,Gif格式

Android Multimedia實戰(四)MediaProjection實現截圖,與MediaMuxer實現錄屏為MP4,Gif格式

MediaProjection可以用來捕捉螢幕,具體來說可以擷取當前螢幕和錄製螢幕視訊 (5.0以上)

先總結下系統是如何實現組合鍵截圖的:
都應該知道Android原始碼中對按鍵的捕獲位於檔案PhoneWindowManager.java中
當滿足按鍵條件時會用一個mHandler 開始post一個runnable,進入這個runnable中執行takeScreenshot()方法。

使用AIDL綁定了service服務到”com.android.systemui.screenshot.TakeScreenshotService”,注意在service連線成功時,對message的msg.arg1和msg.arg2兩個引數的賦值。其中在mScreenshotTimeout中對服務service做了超時處理。接著我們找到實現這個服務service的類TakeScreenshotService,該類在(frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot包下

引用SurfaceControl類,呼叫了screenshot方法, 傳入了螢幕的寬和高,這兩個引數,接著進入SurfaceControl類中,位於frameworks/base/core/java/android/view目錄下

最終到達native方法中nativeScreenshot
面就是java層的部分,接著到jni層,在\frameworks\base\core\jni\android_view_SurfaceControl.cpp中

到jni中,對映nativeScreenshot方法的是nativeScreenshotBitmap函式

最後輾轉來到c++層,就是\frameworks\native\libs\gui下的SurfaceComposerClient.cpp中,實現ScreenshotClient宣告的函式update

當進入到CAPTURE_SCREEN中,data會讀取IGraphicBufferProducer生成出的影象buffe,接著呼叫 reply->writeInt32(res);返回給client.然後再回調到java層。以上就是系統截圖的原理。

那對於多媒體這塊可以通過MediaProjection來實現截圖
實現思路:

首先獲取MediaProjectionManager,和其他的Manager一樣通過 Context.getSystemService() 傳入引數MEDIA_PROJECTION_SERVICE獲得例項。

接著呼叫MediaProjectionManager.createScreenCaptureIntent()彈出dialog詢問使用者是否授權應用捕捉螢幕,同時覆寫onActivityResult()獲取授權結果。

如果授權成功,通過MediaProjectionManager.getMediaProjection(int resultCode, Intent resultData)獲取MediaProjection例項,通過MediaProjection.createVirtualDisplay(String name, int width, int height, int dpi, int flags, Surface surface, VirtualDisplay.Callback callback, Handler handler)建立VirtualDisplay例項。實際上在上述方法中傳入的surface引數,是真正用來截圖或者錄屏的。

截圖

截圖這裡用到ImageReader類,這個類的getSurface()方法獲取到surface直接傳入MediaProjection.createVirtualDisplay()方法中,此時就可以執行擷取。通過ImageReader.acquireLatestImage()方法即可獲取當前螢幕的Image,經過簡單處理之後即可儲存為Bitmap。


    private void startVirtual() {
        if (mMpj != null) {
            virtualDisplay();
        } else {
            setUpMediaProjection();
            virtualDisplay();
        }
    }

    private void setUpMediaProjection() {
        int resultCode = ((MyApplication) getApplication()).getResultCode();
        Intent data = ((MyApplication) getApplication()).getResultIntent();
        mMpj = mMpmngr.getMediaProjection(resultCode, data);
    }

    private void virtualDisplay() {
        mVirtualDisplay = mMpj.createVirtualDisplay("capture_screen", windowWidth, windowHeight, screenDensity,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
    }


 private void startCapture() {
        mImageName = System.currentTimeMillis() + ".png";
        Log.e(TAG, "image name is : " + mImageName);
        Image image = mImageReader.acquireLatestImage();
        int width = image.getWidth();
        int height = image.getHeight();
        final Image.Plane[] planes = image.getPlanes();
        final ByteBuffer buffer = planes[0].getBuffer();
        int pixelStride = planes[0].getPixelStride();
        int rowStride = planes[0].getRowStride();
        int rowPadding = rowStride - pixelStride * width;
        Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(buffer);
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
        image.close();

        if (bitmap != null) {
            Log.e(TAG, "bitmap  create success ");
            try {
                File fileFolder = new File(mImagePath);
                if (!fileFolder.exists())
                    fileFolder.mkdirs();
                File file = new File(mImagePath, mImageName);
                if (!file.exists()) {
                    Log.e(TAG, "file create success ");
                    file.createNewFile();
                }
                FileOutputStream out = new FileOutputStream(file);
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
                out.flush();
                out.close();
                Log.e(TAG, "file save success ");
                Toast.makeText(this.getApplicationContext(), "截圖成功", Toast.LENGTH_SHORT).show();
            } catch (IOException e) {
                Log.e(TAG, e.toString());
                e.printStackTrace();
            }
        }
    }

錄屏1 mp4
主體思路:

邏輯:錄屏不需要操作視訊原始資料,因此使用InputSurface作為編碼器的輸入。

視訊:MediaProjection通過createVirtualDisplay建立的VirtualDisplay獲取當前螢幕的資料。然後傳入到MediaCodec中(即傳入的Surface是通過MediaCodec的createInputSurface方法返回的),然後MediaCodec對資料進行編碼,於是只需要在MediaCodec的輸出緩衝區中拿到編碼後的ByteBuffer即可。

簡單說就是重定向了螢幕錄製的資料的方向,這個Surface提供的是什麼,錄製的視訊資料就傳到哪裡。Surface提供的是本地某個SurfaceView控制元件,那麼就會將螢幕內容顯示到這個控制元件上,提供MediaCodec就是作為編碼器的輸入源最終獲得編碼後的資料,提供ImageReader就會作為ImageReader的資料來源,最終獲得了視訊的原始資料流。

音訊:錄製程式獲得音訊原始資料PCM,傳給MediaCodec編碼,然後從MediaCodec的輸出緩衝區拿到編碼後的ByteBuffer即可。

最終通過合併模組MediaMuxer將音視訊混合。

   小結:錄屏需要用到MediaCadec,這個類將原始的螢幕資料編碼,在通過MediaMuxer分裝為mp4格式儲存。MediaCodec.createInputSurface()獲取一個surface物件,傳入MediaProjection.createVirtualDisplay()即可獲取螢幕原始多媒體資料.之後讀取MediaCodec編碼輸出資料經過MediaMuxer封裝處理為mp4即可播放,實現錄屏。
private void recordVirtualDisplay() {
        while (!mIsQuit.get()) {
            int index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);
            Log.i(TAG, "dequeue output buffer index=" + index);
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//後續輸出格式變化
                resetOutputFormat();
            } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {//請求超時
                Log.d(TAG, "retrieving buffers time out!");
                try {
                    // wait 10ms
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
            } else if (index >= 0) {//有效輸出
                if (!mMuxerStarted) {
                    throw new IllegalStateException("MediaMuxer dose not call addTrack(format) ");
                }
                encodeToVideoTrack(index);
                mMediaCodec.releaseOutputBuffer(index, false);
            }
        }
    }

   private void resetOutputFormat() {
        // should happen before receiving buffers, and should only happen once
        if (mMuxerStarted) {
            throw new IllegalStateException("output format already changed!");
        }
        MediaFormat newFormat = mMediaCodec.getOutputFormat();

        Log.i(TAG, "output format changed.\n new format: " + newFormat.toString());
        mVideoTrackIndex = mMuxer.addTrack(newFormat);
        mMuxer.start();
        mMuxerStarted = true;
        Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex);
    }

錄屏2 Gif
由於錄製的是視訊,得變成gif,有兩種方案:

•提取視訊檔案->解析視訊->提取 Bitmap 序列(使用 MediaMetadataRetriever 提取某一時刻的圖片,然後把很多某一時刻的圖片串聯起來編碼成 gif。看來其也正是 gif 的原理,但實現出來的效果極差,無法準確提取到準確的圖片,導致合成的 gif 圖也無法連貫播放,播放起來也跳幀跳得很厲害。慘不忍睹)

•利用FFmpeg直接轉gif, 這種方法崗崗的。
之前我們演示過:
windows下編譯最新版ffmpeg3.3-android,並通過CMake方式移植到Android studio2.3中 :http://blog.csdn.net/king1425/article/details/70338674
呼叫相關命令也可通過Jni實現。