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