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實現。

github:https://github.com/WangShuo1143368701/VideoView/tree/master/mediaprojectionmediamuxer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章