深入淺出,Andorid 端屏幕采集技術(shù)實(shí)踐
背景
屏幕采集流程

一、獲取MediaProjection
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);

用戶允許(點(diǎn)擊立即開始)后,在 onActivityResult 回調(diào)里根據(jù)返回的resultCode和 data 獲取 MediaProjection:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SCREEN_CAPTURE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
}
}
java.lang.SecurityException: Media projections require a foreground service
of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
if (REQUIRE_FG_SERVICE_FOR_PROJECTION //1.默認(rèn)為true
&& requiresForegroundService() //2.當(dāng)前APP需要啟動(dòng)前臺(tái)Service
&& !mActivityManagerInternal.hasRunningForegroundService( //3.當(dāng)前應(yīng)用沒有啟動(dòng)前臺(tái)service
uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
//APP TargetSdkVersion大于等于29并且不是特權(quán)應(yīng)用(特權(quán)應(yīng)用一般是系統(tǒng)應(yīng)用),則返回true(需要啟動(dòng)前臺(tái)service)
boolean requiresForegroundService () {
return mTargetSdkVersion >= Build.VERSION_CODES.Q && !mIsPrivileged;
}
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--Service命名自定義,這里僅供參考-->
<service
android:name=".ScreenCapturerService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"/>
Surface surface = mediaRecorder.getSurface();Surface surface = mediaCodec.createInputSurface();SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
Surface surface = surfaceView.getHolder().getSurface();
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
}
}, handler);
Surface surface = new Surface(surfaceTexture);
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int dpi,
int flags, Surface surface, VirtualDisplay.Callback callback, Handler handler) {
DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
return dm.createVirtualDisplay(this, name, width, height, dpi, surface, flags, callback,
handler, null /* uniqueId */);
}
參數(shù)說明文檔如下:

各參數(shù) Android 官方文檔都有較詳細(xì)的說明,其中 flag 和 surface 這里再額外說明下:
flag是VirtualDisplay的標(biāo)記位,一般取VIRTUAL_DISPLAY_FLAG_PUBLIC即可; surface 也就是上文提到的屏幕數(shù)據(jù)緩沖區(qū),一般由消費(fèi)者提供。
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
dealTextureFrame();
}
private void dealTextureFrame() {
...
surfaceTexture.updateTexImage();
float[] transformMatrix = new float[16];
surfaceTexture.getTransformMatrix(transformMatrix);
...
}
屏幕共享(錄屏直播)時(shí),高分辨率代表著清晰度,高幀率代表著流暢度。魚和熊掌,往往不可兼得,尤其是在網(wǎng)絡(luò)、設(shè)備性能受限的情況下。
當(dāng)手機(jī)屏幕在某個(gè)界面靜止或者界面低速運(yùn)動(dòng)時(shí),我們以較低的幀率抓取屏幕即可讓接收方觀看時(shí)不至于產(chǎn)生卡頓掉幀感,這時(shí)可以適當(dāng)提升屏幕采集分辨率,讓畫質(zhì)更清晰;相反如果是游戲直播等屏幕界面快速運(yùn)動(dòng)等場景,則需要以較高幀率抓取屏幕內(nèi)容才能讓接收方有順滑觀看體驗(yàn),但在資源受限情況下,可能需要犧牲部分清晰度為代價(jià)。
屏幕采集分辨率的控制較為簡單,在第三步創(chuàng)建 VirtualDisplay 時(shí),傳入需要的 width 和 height 值即可。
屏幕采集幀率的上限取決以 Android 設(shè)備的屏幕刷新率,下限是0,即丟棄所有返回?cái)?shù)據(jù)不處理。采集幀率并不是越高越好,夠用就行。比如在低端機(jī)上,就算以較高幀率采集屏幕數(shù)據(jù),但受限于機(jī)器編解碼能力,實(shí)際上屏幕傳輸?shù)膸蔬_(dá)不到采集幀率,反而會(huì)消耗過多系統(tǒng)資源導(dǎo)致發(fā)熱、卡頓等現(xiàn)象。這時(shí)候就需要適當(dāng)降低采集幀率。還是以第二步中通過 SurfaceTexture 生成的Surface 為例,在 onFrameAvailable 回調(diào)里,以特定算法有規(guī)律地丟棄部分?jǐn)?shù)據(jù),從而降低采集幀率。
六、橫豎屏切換
橫豎屏切換的場景在游戲直播中屢見不鮮。比如王者榮耀的主播切換賬號(hào)時(shí),需要先kill掉王者榮耀 APP 退到手機(jī)主界面,然后再打開王者榮耀重新登錄,經(jīng)歷了從橫屏到豎屏再回到橫屏的切換。
屏幕采集當(dāng)然也需要根據(jù)不同的橫豎屏模式來做動(dòng)態(tài)調(diào)整。調(diào)整的前提是如何感知到橫豎屏模式的變化。
如果是監(jiān)聽手機(jī)物理方向上的翻轉(zhuǎn),使用 OrientationEventListener 即可。但是針對(duì)某些強(qiáng)制橫屏的 APP,比如王者榮耀,將手機(jī)平放在水平桌面上直接打開這些 APP,進(jìn)入 APP 后的界面是橫屏展示的,這時(shí)通過 OrientationEventListener 檢測出來的角度變化無法判斷 APP 界面是否橫屏展示。
實(shí)際上,我們需要感知的是當(dāng)前屏幕界面橫豎屏展示狀態(tài)而非手機(jī)物理上橫豎翻轉(zhuǎn)狀態(tài)。
這時(shí)我們就需要根據(jù) Display 的 rotation 值來判斷界面的橫豎屏狀態(tài),rotation 有以下值:
public static final int ROTATION_0 = 0; //默認(rèn)豎直狀態(tài)
public static final int ROTATION_90 = 1; //左橫屏
public static final int ROTATION_180 = 2; //倒立
public static final int ROTATION_270 = 3; //右橫屏
其中ROTATION_0和ROTATION_180代表豎屏的兩種狀態(tài),ROTATION_90和ROTATION_270代表橫屏的兩種狀態(tài)。我們只關(guān)心是界面否經(jīng)歷了橫豎屏狀態(tài)的切換,至于左橫屏還是右橫屏,并不影響采集效果。
private boolean checkRotationChange() {
int currentRotation = display.getRotation();
boolean rotationChange = false;
if ((currentRotation + lastRotation) % 2 == 1) {
rotationChange = true;
}
lastRotation = currentRotation;
return rotationChange;
}
總結(jié)
本文針對(duì) Android 端屏幕采集涉及到的屏幕數(shù)據(jù)生產(chǎn)者,數(shù)據(jù)緩沖區(qū)做了簡單介紹,其實(shí)消費(fèi)者對(duì)屏幕原始數(shù)據(jù)的處理更是整個(gè)屏幕共享流程中關(guān)鍵的步驟。另外對(duì)屏幕采集的分辨率、幀率的控制,橫豎屏切換適配等問題也只是理論上闡述,具體代碼實(shí)現(xiàn)還是有很多細(xì)節(jié)需要注意。
