<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 6623字,需瀏覽 14分鐘

           ·

          2021-05-22 16:49

          隨著全球產(chǎn)業(yè)鏈線上化和數(shù)字化的加速,移動(dòng)端實(shí)時(shí)屏幕共享在各行各業(yè)場景下都有了廣泛的應(yīng)用,比如在線教育、視頻會(huì)議、遠(yuǎn)程業(yè)務(wù)咨詢、手游直播。而屏幕采集則是實(shí)現(xiàn)實(shí)時(shí)屏幕共享流程中的第一步,本篇技術(shù)分享就來跟大家講講拍樂云在 Andorid 端屏幕采集的經(jīng)驗(yàn)實(shí)踐。

            背景

          Android 從 4.0 開始就提供了手機(jī)錄屏方法,但是需要 root 權(quán)限。從 5.0 開始,Google 開放了系統(tǒng)錄屏API:MediaProjection 和 MediaProjectionManager,不需要 root 權(quán)限,但是會(huì)彈出錄屏權(quán)限申請(qǐng)框,用戶同意后才能開始錄屏,類似 Android6.0 之后權(quán)限申請(qǐng)流程。
          鑒于目前市面上5.0以下的 Android 手機(jī)占比很低且屏幕采集需要 root 權(quán)限實(shí)現(xiàn)復(fù)雜,接下來我們主要介紹 Android5.0 及以上版本的屏幕采集原理。
          試想一下,一套完整的屏幕采集流程應(yīng)該是怎樣的?屏幕數(shù)據(jù)源(生產(chǎn)者)在緩沖區(qū)產(chǎn)生數(shù)據(jù),屏幕數(shù)據(jù)消費(fèi)者從緩沖區(qū)提取數(shù)據(jù)使用。不同的消費(fèi)者可以實(shí)現(xiàn)不同的功能,比如錄屏保存和錄屏直播(屏幕共享)。這些關(guān)鍵的角色在Android 端又是由誰來扮演呢?
          VirtualDisplayVirtualDisplay 是 Android 上的虛擬顯示器。本文里VirtualDisplay 的作用就是抓取屏幕上顯示的內(nèi)容,是屏幕數(shù)據(jù)的生產(chǎn)者。
          Surface 在 Android 的窗口實(shí)現(xiàn)里,Surface 對(duì)應(yīng)了一塊屏幕數(shù)據(jù)緩沖區(qū),屏幕數(shù)據(jù)生產(chǎn)者可以在 Surface 上生產(chǎn)數(shù)據(jù),消費(fèi)者則從 Surface 中提取數(shù)據(jù)使用。

            屏幕采集流程

          介紹完以上關(guān)鍵角色,我們大致可以畫出一套屏幕采集流程圖:

          下面逐步介紹代碼實(shí)現(xiàn)。

          一、獲取MediaProjection

          首先需要獲取 MediaProjectionManager 服務(wù),然后通過 MediaProjectionManager 服務(wù),獲取一個(gè)申請(qǐng)屏幕采集權(quán)限的 Intent 并啟動(dòng)屏幕采集申請(qǐng)權(quán)限界面:
          mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
          Intent intent = mediaProjectionManager.createScreenCaptureIntent();
          startActivityForResult(intent, SCREEN_CAPTURE_REQUEST_CODE);
          啟動(dòng)的屏幕采集權(quán)限申請(qǐng)界面如下

          用戶允許(點(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);
            }
          }
          需要特別注意的是,在 targetSdkVersion 大于等于29時(shí),系統(tǒng)加強(qiáng)了對(duì)屏幕采集的限制,必須先啟動(dòng)相應(yīng)的前臺(tái) Service,才能正常調(diào)用 getMediaProjection 方法,否則會(huì)拋異常:
          java.lang.SecurityException: Media projections require a foreground service 
          of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
          查看系統(tǒng)源碼發(fā)現(xiàn)以下條件語句如果都為 true 則拋出以上異常:
          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;
          }
          前臺(tái) Service 配置參考如下:
          <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

          <!--Service命名自定義,這里僅供參考-->
          <service
            android:name=".ScreenCapturerService"
            android:enabled="true"
            android:foregroundServiceType="mediaProjection"/>
          二、構(gòu)造Surface
          1.如果屏幕采集數(shù)據(jù)用來錄制視頻,那么消費(fèi)者可以是 MediaRecoder,相應(yīng)地 Surface 由 MediaRecoder 提供:
          Surface surface = mediaRecorder.getSurface();
          2.如果屏幕采集數(shù)據(jù)用來屏幕共享(錄屏直播),那么消費(fèi)者可以是類似 MediaCodec 這樣的編碼器,相應(yīng)地 Surface 由 MediaCodec 提供:
          Surface surface = mediaCodec.createInputSurface();
          3.如果需要將屏幕采集數(shù)據(jù)顯示在UI界面 SurfaceView 上的話,Surface可以通過以下方式生成:
          SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
          Surface surface = surfaceView.getHolder().getSurface();
          4.如果想要更加靈活的掌控整個(gè)屏幕采集流程,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 可以用來捕獲視頻流中的圖像幀,當(dāng) SurfaceTexture 中有數(shù)據(jù)更新時(shí),會(huì)觸發(fā)onFrameAvailable 回調(diào),此時(shí)可以調(diào)用 updateTexImage 方法從視頻流數(shù)據(jù)中更新當(dāng)前數(shù)據(jù)幀。
          三、創(chuàng)建VirtualDisplay
          MediaProjection 有現(xiàn)成的API可以調(diào)用:
          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)者提供。
          四、屏幕采集數(shù)據(jù)處理
          我們以第二步中通過 SurfaceTexture 生成的 Surface 為例。當(dāng) SurfaceTexture 中有數(shù)據(jù)更新時(shí),會(huì)觸發(fā) onFrameAvailable 回調(diào),我們可以在該回調(diào)里對(duì)數(shù)據(jù)進(jìn)行特定的處理。
          @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é)需要注意。


          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99热精品欧美亚洲 | 青春草视频在线 | 成人欧美在线 | 美女扒开粉嫩尿囗的桶爽www | 影音先锋东京热 |