1. 程式人生 > >EasyPusher 結合Android Architecture Component便捷開發二

EasyPusher 結合Android Architecture Component便捷開發二

上一篇部落格我們簡單介紹了一下Android Architecture Component的相關概念與知識點,這篇部落格我們將介紹一下如何根據其改造EasyPusher.

EasyPusher的業務邏輯模組是MediaStream類,該類實現攝像頭的開啟關閉,音訊採集的開啟關閉,推送的開始和停止的功能.

我們先看看EasyPusher主介面原來的一些關鍵處理邏輯:
1. onCreate裡面進行許可權檢查,如果尚未獲取許可權,那彈出獲取視窗;
2. onResume裡面,檢查許可權,如果未獲取,什麼也不做;否則檢查Texture,如果有效,開啟預覽,否則什麼也不做;
3. onTextureAvailable ,檢查是否有許可權,如果沒有,什麼也不做,否則開啟攝像頭
4. onPause裡面,如果之前開啟了預覽,那關閉預覽;
5. onRequestPermissionsResult,如果許可權獲取到了,那麼再嘗試開啟攝像頭.
6. onDestory裡面,檢查是否已經開啟了.如果沒有,什麼也不做,否則釋放相關資源.
7. 對MediaStream設定一些回撥,或者捕獲事件,進行MediaStream的狀態更新.

綜上,可以看到,為了開啟攝像頭,我們真是煞費苦心,不得不在若干地方進行若干種不同的條件檢查.同時,在銷燬的時候,也得做出許多非空判斷.這導致我們的上層邏輯比較複雜,主Activity的程式碼裡有將近800行.

甚至,之前的Pusher還不支援橫豎屏切換的功能,如果支援了,在Activity recreate的時候,要保持攝像頭和推送的狀態,可能又會帶來更多的程式碼和BUG隱患.

這麼多狀態檢查很痛苦,我們極度期望當我們呼叫了開始預覽後,

==MediaStream能夠在內部進行狀態檢查,在狀態都準備好後,自動啟動.==

同時,在我們退出Activity後,

==MediaStream能夠自動進行資源釋放並優雅退出==

基於Architecture Component,MediaStream便可滿足上面的期望.我們將對MediaStream做出如下改動:
1. 繼承ViewModel.這樣使得MediaStream的在Activity的狀態更改(比如橫豎屏切換)時能自動維護狀態;並且在Activity 銷燬時,自動得知,從而自己反初始化自己.

    // MediaStream.java
    // 在Activity Destory的時候,onCleared會自動被呼叫.這裡進行資源釋放和自我反初始化
    @Override
    protected void onCleared() {
        super
.onCleared(); stopStream(); // 停止推流 stopPreview(); // 停止預覽 destroyCamera(); // 關閉攝像頭 release(); // 反初始化自己 }
  1. 上層需要更新的狀態通過LiveData進行通知.這裡主要有攝像頭的當前解析度\推送開啟狀態\推送狀態\需要監聽.因此我們在MediaStream裡定義三個LiveData.分別用來表示攝像頭解析度\推送開始狀態\推送狀態的觀察者.
    private final CameraPreviewResolutionLiveData cameraPreviewResolution;
    private final PushingStateLiveData pushingStateLiveData;
    private final StreamingStateLiveData streamingStateLiveData;

同時給上層匯出三個LiveData的觀察介面:

    @MainThread
    public void observeCameraPreviewResolution(LifecycleOwner owner, Observer<int[]> observer) {
        cameraPreviewResolution.observe(owner, observer);
    }

    @MainThread
    public void observePushingState(LifecycleOwner owner, Observer<PushingState> observer) {
        pushingStateLiveData.observe(owner, observer);
    }

    @MainThread
    public void observeStreamingState(LifecycleOwner owner, Observer<Boolean> observer) {
        streamingStateLiveData.observe(owner, observer);
    }

這樣上層Activity可觀察這些介面,並進行處理.

觀察解析度更改:

        model.observeCameraPreviewResolution(this, new Observer<int[]>() {
            @Override
            public void onChanged(@Nullable int[] size) {
                Toast.makeText(MainActivity.this,"當前攝像頭解析度為:" + size[0] + "*" + size[1], Toast.LENGTH_SHORT).show();
            }
        });

觀察推送狀態更改:

        model.observePushingState(this, new Observer<MediaStream.PushingState>(){

            @Override
            public void onChanged(@Nullable MediaStream.PushingState pushingState) {
                pushingStateText.setText(pushingState.msg);
                if (pushingState.state > 0){
                    pushingStateText.append(String.format("rtsp://cloud.easydarwin.org:554/test123456.sdp"));
                }
            }
        });

觀察推送使能啟停狀態更改:

        model.observeStreamingState(this, new Observer<Boolean>(){

            @Override
            public void onChanged(@Nullable Boolean aBoolean) {
                pushingBtn.setText(aBoolean ? "停止推送":"開始推送");
            }
        });

相比普通回撥而言,LiveData是’粘性(sticky)’的,也就是在上層開始註冊它的時候,它會把自己的最新狀態回撥一次.因此就沒必要再主動查詢一次了(相信大家都比較討厭這種模式).這樣是不是很方便?
3. 實現LifecyclerObserver.這個介面並沒有任何實現方法,而是用annotation進行資料同步的.其原始碼如下:

/**
 * Marks a class as a LifecycleObserver. It does not have any methods, instead, relies on
 * {@link OnLifecycleEvent} annotated methods.
 * <p>
 * @see Lifecycle Lifecycle - for samples and usage patterns.
 */
@SuppressWarnings("WeakerAccess")
public interface LifecycleObserver {

}

MediaStream通過一個介面openCameraPreview開啟攝像頭,該介面根據當前時機判斷是否可以開啟攝像頭了.如果可以,就開啟;如果不可以,留個開啟標識,後面時機成熟了,再開啟.

    @MainThread
    public void openCameraPreview(){
        cameraOpened = true;
        if (cameraCanOpenNow()) {
            createCamera();
            startPreview();
        }
    }
    // 判斷攝像頭當前是否可以開啟了?
    // 條件:Activity Started,許可權允許,Texture有效
    private boolean cameraCanOpenNow() {
        if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
            if (ActivityCompat.checkSelfPermission(getApplication(), android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED ||
                    ActivityCompat.checkSelfPermission(getApplication(), android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
                // connect if not connected
                if (mSurfaceHolderRef != null && mSurfaceHolderRef.get() != null) {
                    return true;
                }
            }
        }
        return false;
    }

那有哪些地方檢查”時機成熟”呢?
根據檢查條件,應該有三個:1 Activity Started的時候;2 設定了SurfaceTexture的時候;3 許可權獲取成功的時候;我們逐一說明:

MediaStream實現了LifecycleObserver後,需要關注Activity的啟動和終止,增加這樣兩個註解函式(annotated method),這裡我們可以監控到Activity Start,可以看到,在start的時候,再嘗試開啟一次攝像頭.同理,在onStop的時候,嘗試關閉攝像頭.

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void start() {
        if (cameraOpened) openCameraPreview();
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void stop() {
        if (cameraOpened) closeCameraPreview();
    }

我們還需要一個設定SurfaceTexture的介面,這裡再嘗試開啟一次攝像頭.


    @MainThread
    public void setSurfaceTexture(SurfaceTexture texture) {
        if (texture == null) {
            stopPreview();
            mSurfaceHolderRef = null;
        }else {
            mSurfaceHolderRef = new WeakReference<SurfaceTexture>(texture);
            stopPreview();
            if (cameraOpened) openCameraPreview();
        }
    }

最後,就是上層獲取到許可權的時候,通知MediaStream,再嘗試一次.


    @MainThread
    public void notifyPermissionGranted(){
        if (cameraOpened) openCameraPreview();
    }

實現了LifecyclerObserver後,我們需要將Observer註冊到Lifecycle,Activity層在獲取到MediaStream後,需要呼叫setLifecycle.使得MediaStream能夠監聽到其狀態變更.


    public void setLifecycle(Lifecycle lifecycle){
        this.lifecycle = lifecycle;
        lifecycle.addObserver(this);
    }

至此,我們將MediaStream改造完成.然後我們再看看上層該如何呼叫吧!

現在,上層呼叫實在太簡單了.

  • 在onCreate裡面獲取MediaStream例項, 呼叫setLifecycle將自己的生命週期註冊給MediaStream這個觀察者;
  • 觀察MediaStream裡面的一些狀態更改並作出UI提示;
  • 嘗試啟動攝像頭;
  • 檢查並獲取攝像頭許可權,並且在許可權獲取完成時,告知MediaStream;
  • 在SurfaceTexture準備好時,設定給MediaStream
  • 一個按鈕,控制推送開始\停止

整個Activity的程式碼如下,清晰又簡潔:

package com.example.myapplication;

import android.arch.lifecycle.LifecycleActivity;
import android.arch.lifecycle.Observer;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.support.annotation.Nullable;
import android.os.Bundle;
import android.arch.lifecycle.ViewModelProviders;
import android.support.v4.app.ActivityCompat;
import android.view.TextureView;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.easydarwin.push.MediaStream;

public class MainActivity extends LifecycleActivity {

    private static final int REQUEST_CAMERA_PERMISSION = 1000;
    private MediaStream mediaStream;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mediaStream = ViewModelProviders.of(this).get(MediaStream.class);
        mediaStream.setLifecycle(getLifecycle());
        mediaStream.openCameraPreview();

        mediaStream.observeCameraPreviewResolution(this, new Observer<int[]>() {
            @Override
            public void onChanged(@Nullable int[] size) {
                Toast.makeText(MainActivity.this,"當前攝像頭解析度為:" + size[0] + "*" + size[1], Toast.LENGTH_SHORT).show();
            }
        });
        final TextView pushingStateText = findViewById(R.id.pushing_state);
        final TextView pushingBtn = findViewById(R.id.pushing);
        mediaStream.observePushingState(this, new Observer<MediaStream.PushingState>(){

            @Override
            public void onChanged(@Nullable MediaStream.PushingState pushingState) {
                pushingStateText.setText(pushingState.msg);
                if (pushingState.state > 0){
                    pushingStateText.append(String.format("rtsp://cloud.easydarwin.org:554/test123456.sdp"));
                }
            }
        });
        mediaStream.observeStreamingState(this, new Observer<Boolean>(){

            @Override
            public void onChanged(@Nullable Boolean aBoolean) {
                pushingBtn.setText(aBoolean ? "停止推送":"開始推送");
            }
        });

        TextureView textureView = findViewById(R.id.texture_view);
        textureView.setSurfaceTextureListener(new SurfaceTextureListenerWrapper() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
                mediaStream.setSurfaceTexture(surfaceTexture);
            }
        });


        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA,android.Manifest.permission.RECORD_AUDIO}, REQUEST_CAMERA_PERMISSION);
        }
    }

    public void onPushing(View view) {
        MediaStream.PushingState state = mediaStream.getPushingState();
        if (state != null && state.state > 0){
            mediaStream.stopStream();
        }else {
            mediaStream.startStream("cloud.easydarwin.org", "554", "test123456");
        }
    }


    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CAMERA_PERMISSION: {
                if (grantResults.length > 1
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
                    mediaStream.notifyPermissionGranted();
                } else {
                    finish();
                }
                break;
            }
        }
    }
}

大家可以發現,關鍵程式碼其實就那麼幾行.
最後,作者喊出這句話:

十行程式碼行程式碼實現一個Android Pusher!

應該不會被打吧(/偷笑 /偷笑 /偷笑)?