1. 程式人生 > >Android實現錄屏直播(二)需求才是硬道理之產品功能調研

Android實現錄屏直播(二)需求才是硬道理之產品功能調研

前面的Android實現錄屏直播(一)ScreenRecorder的簡單分析一文中我們對 ScreenRecorder 這個開源 Demo 中的實現機制大概有了瞭解,但在繼續寫這個系列文章的時候發現每一個細節都太緊密了,稍微不注意就會深入每個知識點的細節導致文章又臭又長還表述不清晰,於是我決定把這7天實現該功能的整個流程重新梳理一遍,按照我開發和研究學習的步驟來寫,大致過程如下:

  1. Bilibili 的反編譯及 UI 的編寫
  2. 對 H264 結構、FLV 格式封裝的研究學習
  3. sps pps avcc 關鍵幀等視訊封裝原理的學習與分析
  4. MediaProjection 實現錄屏中 MediaCodec 的詳細用法

產品功能調研

我們作為技術開發人員,任務下發的時候首先要與產品經理進行需求的深入瞭解,只有瞭解對方想要的是什麼後我們功能實現才能達到他們最大的期望值。當然一旦確定需求後把菜刀亮出來,然後就輕鬆愉快的寫程式碼吧��。嗯,本次任務就是儘可能的還原Bilibili的錄屏直播功能,汗顏,無需設計,無需討論,我自己研究吧,反正專案一直都是我一人開發,也習慣了(PS: 儘管是Bilibili的忠實使用者,當然不能在工作的時候看番劇啦),廢話不多說,瞄準幾個功能:

  • 直播懸浮窗
  • 直播提示通知欄
  • 錄屏直播的實現機制

直播懸浮窗

直播懸浮窗

懸浮窗包含一個自定義的View(繼承自FrameLayout)、服務和自定義WindowManager,更多細節請見原始碼,程式碼就不貼出來了,太佔地方。值得注意的是,這裡的懸浮窗我添加了一個彈幕顯示的ListView面板用於顯示使用者的發言。

直播提示通知欄

由於直接錄製桌面涉及到使用者的個人隱私問題,如果沒有一個標識讓使用者看著那問題就麻煩了(親測過通過這種錄屏方式可以捕獲桌面所有的內容,無論是鍵盤輸入狀態、攝像頭拍照攝像及輸入銀行卡密碼等等),所以Android系統雖然會提示需要進行螢幕錄影,但還是有一定的風險!通知欄屬於常駐型別,內容如下:

錄屏通知欄

通知欄的實現

先看看下面這個服務類,主要功能就是開啟通知並建立懸浮窗兩個功能,注意建立懸浮窗時判斷當前裝置是否為我們的APP授權了懸浮窗許可權這個功能我還沒有來得及實現,小米、魅族華為等系統需要手動去APP設定中授權,之後有時間再補上。如需此服務與Activity之間有通訊機制的話,還需要自定義Binder,在啟動服務的Activity中通過bindService()與ServiceConnection來實現資料的傳遞,這裡先挖一個坑,後續我完善功能後再回來填上。

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.os.Binder;
import android.os.Handler;
import android.support.v4.app.NotificationCompat;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class ScreenRecordListenerService extends Service {
    private static final String TAG = "ScreenRecordListenerService, ";
    private static final int PENDING_REQUEST_CODE = 0x01;
    private static final int NOTIFICATION_ID = 3;
    private NotificationManager mNotificationManager;
    private NotificationCompat.Builder builder;
    private Handler handler = new Handler();

    @Override
    public void onCreate() {
        super.onCreate();
        initNotification();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 當前介面是桌面,且沒有懸浮窗顯示,則建立懸浮窗。
        if (!MyWindowManager.isWindowShowing()) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    MyWindowManager.createSmallWindow(getApplicationContext());
                }
            });
        }
        return super.onStartCommand(intent, flags, startId);
    }


    private void initNotification() {
        builder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle(getResources().getString(R.string.app_name))
                .setContentText("您正在錄製視訊內容哦")
                .setOngoing(true)
                .setDefaults(Notification.DEFAULT_VIBRATE);

        Intent backIntent = new Intent(this, RecordActivityV2.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, PENDING_REQUEST_CODE, backIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(pendingIntent);
        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        mNotificationManager.notify(NOTIFICATION_ID, builder.build());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mNotificationManager != null) {
            mNotificationManager.cancel(NOTIFICATION_ID);
        }
        MyWindowManager.removeSmallWindow(getApplicationContext());
    }
}

上面的程式碼我們可以看出服務啟動後進行了Notification的初始化,我這裡按照Bilibili的樣式去實現的。在Activity中當啟動直播時,Bilibili是通過moveTaskToBack(true)直接回到桌面的,所以我猜想開啟通知欄和懸浮窗服務應該是在Activity生命週期的onStop()啟動和onResume()關閉,當然這些都是業務邏輯可自己定義。

@Override
protected void onResume() {
    super.onResume();
    if (isRecording) stopScreenRecordService();
}

@Override
protected void onStop() {
    super.onStop();
    if (isRecording) startScreenRecordService();
}
 private void startScreenRecordService() {
        if (mRecorder != null && mRecorder.getStatus()) {
            Intent runningServiceIT = new Intent(this, ScreenRecordListenerService.class);
           // bindService(runningServiceIT, connection, BIND_AUTO_CREATE); 
            startService(runningServiceIT);
        }
}

private void stopScreenRecordService() {
    Intent runningServiceIT = new Intent(this, ScreenRecordListenerService.class);
    stopService(runningServiceIT);
    if (mRecorder != null && mRecorder.getStatus()) {
        Toast.makeText(this, "現在正在進行錄屏直播哦", Toast.LENGTH_SHORT).show();
    }
}

最後別忘了在AndroidManifest.xml中進行Service的註冊和對懸浮窗的授權。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
    <service
        android:name=".service.ScreenRecordListenerService"
        android:enabled="true"
        android:exported="false" />
</application>

關於懸浮窗與通知欄差不多就介紹到這裡了,更多細節請檢視原始碼,本Demo原始碼都是基於上篇部落格提到的ScreenRecorder實現的。

我們來看看最終效果吧:

效果圖

錄屏直播的實現機制

使用MediaProjection與VirtualDisplay等Android 5.0以上的API實現的,在此就不多說了,請看上篇部落格吧。

尾語

果然還是有個提綱要好下筆的多,至少知道寫些什麼。因為最近文章寫的都有點趕,有疑問的朋友還請留言指正!接下來準備寫一下MediaProjection 實現錄屏中 MediaCodec 的詳細用法,至於反編譯Bilibili的過程就不寫了,反編譯為的是參考別人的思路,並不是為了做程式碼小偷,這點職業操守還是有的,如果說業務上有盜竊之意請出門左轉找我們的產品經理,我是無辜的碼農[笑]。之後會在別的文章中提及一下我個人使用反編譯時的一些心得和小技巧吧。別的主題都還需要花些時間再看看,畢竟都是工作時候囫圇吞棗接觸的,理論知識可不能瞎糊弄。

Demo原始碼都在我的GitHub倉庫中,有需要的朋友可以去 clonefork,如對您有幫助還請給個Star,十分感謝~

參考文件