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