手機端實時屏幕共享在視頻會議、手游直播等場景下有廣泛應用。屏幕采集則是整個實時屏幕共享流程的第一步,下面簡單介紹下Andorid端屏幕采集的原理。
背景
Android從 4.0 開始就提供了手機錄屏方法,但是需要 root 權限。從 5.0 開始,Google開放了系統錄屏API:MediaProjection
和MediaProjectionManager
,不需要root權限,但是會彈出錄屏權限申請框,用戶同意后才能開始錄屏,類似Android6.0之后權限申請流程。
鑒于目前市面上5.0以下的Android手機占比很低且屏幕采集需要root權限實現復雜,接下來我們主要介紹Android5.0及以上版本的屏幕采集原理。
試想一下,一套完整的屏幕采集流程應該是怎樣的?屏幕數據源(生產者)在緩沖區產生數據,屏幕數據消費者從緩沖區提取數據使用。不同的消費者可以實現不同的功能,比如錄屏保存和錄屏直播(屏幕共享)。這些關鍵的角色在Android端又是由誰來扮演呢?
VirtualDisplay
VirtualDisplay
是Android上的虛擬顯示器。本文里VirtualDisplay
的作用就是抓取屏幕上顯示的內容,是屏幕數據的生產者。
Surface
在Android的窗口實現里,Surface
對應了一塊屏幕數據緩沖區,屏幕數據生產者可以在Surface
上生產數據,消費者則從Surface
中提取數據使用。
屏幕采集流程
介紹完以上關鍵角色,我們大致可以畫出一套屏幕采集流程圖:
下面逐步介紹代碼實現。
一、獲取MediaProjection
首先需要獲取MediaProjectionManager
服務,然后通過MediaProjectionManager
服務,獲取一個申請屏幕采集權限的Intent
并啟動屏幕采集申請權限界面:
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);
啟動的屏幕采集權限申請界面如下:
用戶允許(點擊立即開始)后,在
onActivityResult
回調里根據返回的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);
}
}
需要特別注意的是,在targetSdkVersion
大于等于29(Android 10)時,系統加強了對屏幕采集的限制,必須先啟動相應的前臺Service,才能正常調用getMediaProjection
方法,否則會拋異常:
java.lang.SecurityException: Media projections require a foreground service
of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
查看系統源碼發現以下條件語句如果都為true
則拋出以上異常:
if (REQUIRE_FG_SERVICE_FOR_PROJECTION //1.默認為true
&& requiresForegroundService() //2.當前APP需要啟動前臺Service
&& !mActivityManagerInternal.hasRunningForegroundService( //3.當前應用沒有啟動前臺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并且不是特權應用(特權應用一般是系統應用),則返回true(需要啟動前臺service)
boolean requiresForegroundService () {
return mTargetSdkVersion >= Build.VERSION_CODES.Q && !mIsPrivileged;
}
前臺Service配置參考如下:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--Service命名自定義,這里僅供參考-->
<service
android:name=".ScreenCapturerService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"/>
二、構造Surface
1.如果屏幕采集數據用來錄制視頻,那么消費者可以是MediaRecoder
,相應地Surface
由MediaRecoder
提供:
Surface surface = mediaRecorder.getSurface();
2.如果屏幕采集數據用來屏幕共享(錄屏直播),那么消費者可以是類似MediaCodec
這樣的編碼器,相應地Surface
由 MediaCodec
提供:
Surface surface = mediaCodec.createInputSurface();
3.如果需要將屏幕采集數據顯示在UI界面SurfaceView
上的話,Surface
可以通過以下方式生成:
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
Surface surface = surfaceView.getHolder().getSurface();
4.如果想要更加靈活的掌控整個屏幕采集流程,Surface
還可以通過SurfaceTexture
生成:
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
}
}, handler);
Surface surface = new Surface(surfaceTexture);
這里簡單介紹下SurfaceTexture
。SurfaceTexture
可以用來捕獲視頻流中的圖像幀,不同于 SurfaceView
會將圖像顯示在屏幕上,SurfaceTexture
對圖像流的處理并不直接顯示,而是轉為 GL 外部紋理。當SurfaceTexture
中有數據更新時,會觸發onFrameAvailable
回調,此時可以調用updateTexImage
方法從視頻流數據中更新當前數據幀。
三、創建VirtualDisplay
MediaProjection
有現成的API可以調用:
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 */);
}
參數說明文檔如下:
各參數Android官方文檔都有較詳細的說明,其中
flag
和surface
這里再額外說明下:
-
flag
是VirtualDisplay
的標記位,一般取VIRTUAL_DISPLAY_FLAG_PUBLIC
即可; -
surface
也就是上文提到的屏幕數據緩沖區,一般由消費者提供。
四、屏幕采集數據處理
我們以第二步中通過SurfaceTexture
生成的Surface
為例。當SurfaceTexture
中有數據更新時,會觸發onFrameAvailable
回調,我們可以在該回調里對數據進行特定的處理。
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
dealTextureFrame();
}
private void dealTextureFrame() {
...
surfaceTexture.updateTexImage();
float[] transformMatrix = new float[16];
surfaceTexture.getTransformMatrix(transformMatrix);
...
}
五、分辨率、幀率控制
屏幕共享(錄屏直播)時,高分辨率代表著清晰度,高幀率代表著流暢度。在網絡、設備性能受限的情況下,清晰度和流暢度往往不可兼得,我們需要在兩者間做平衡。
當手機屏幕在某個界面靜止或者界面低速運動時,我們以較低的幀率抓取屏幕即可讓接收方觀看時不至于產生卡頓掉幀感,這時可以適當提升屏幕采集分辨率,讓畫質更清晰;相反如果是游戲直播等屏幕界面快速運動等場景,則需要以較高幀率抓取屏幕內容才能讓接收方有順滑觀看體驗,但在資源受限情況下,可能需要犧牲部分清晰度為代價。
屏幕采集分辨率的控制較為簡單,在第三步創建VirtualDisplay
時,傳入需要的width
和height
值即可。
屏幕采集幀率的上限取決以Android設備的屏幕刷新率,下限是0,即丟棄所有返回數據不處理。采集幀率并不是越高越好,夠用就行。比如在低端機上,就算以較高幀率采集屏幕數據,但受限于機器編解碼能力,實際上屏幕傳輸的幀率達不到采集幀率,反而會消耗過多系統資源導致發熱、卡頓等現象。這時候就需要適當降低采集幀率。還是以第二步中通過SurfaceTexture
生成的Surface
為例,在onFrameAvailable
回調里,以特定算法有規律地丟棄部分數據,從而降低采集幀率。
六、橫豎屏切換
橫豎屏切換的場景在游戲直播中屢見不鮮。比如王者榮耀的主播切換賬號時,需要先kill掉王者榮耀APP退到手機主界面,然后再打開王者榮耀重新登錄,經歷了從橫屏到豎屏再回到橫屏的切換。
屏幕采集當然也需要根據不同的橫豎屏模式來做動態調整。調整的前提是如何感知到橫豎屏模式的變化。
如果是監聽手機物理方向上的翻轉,使用OrientationEventListener
即可。但是針對某些強制橫屏的APP,比如王者榮耀,將手機平放在水平桌面上直接打開這些APP,進入APP后的界面是橫屏展示的,這時通過OrientationEventListener
檢測出來的角度變化無法判斷APP界面是否橫屏展示。
實際上,我們需要感知的是當前屏幕界面橫豎屏展示狀態而非手機物理上橫豎翻轉狀態。
這時我們就需要根據Display
的rotation
值來判斷界面的橫豎屏狀態,rotation
有以下值:
public static final int ROTATION_0 = 0; //默認豎直狀態
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
代表豎屏的兩種狀態,ROTATION_90
和ROTATION_270
代表橫屏的兩種狀態。我們只關心是界面否經歷了橫豎屏狀態的切換,至于左橫屏還是右橫屏,并不影響采集效果。
private boolean checkRotationChange() {
int currentRotation = display.getRotation();
boolean rotationChange = false;
if ((currentRotation + lastRotation) % 2 == 1) {
rotationChange = true;
}
lastRotation = currentRotation;
return rotationChange;
}
總結
本文針對Android端屏幕采集涉及到的屏幕數據生產者,數據緩沖區做了簡單介紹,其實消費者對屏幕原始數據的處理更是整個屏幕共享流程中關鍵的步驟。另外對屏幕采集的分辨率、幀率的控制,橫豎屏切換適配等問題也只是理論上闡述,具體代碼實現還是有很多細節需要注意。