視頻播放基礎控件與無縫技術介紹

視頻播放基礎控件與無縫技術介紹

概述

   前陣子經手了項目裏 Feed 流短視頻播放的工作,雖然技術上難度不算大,但實現還是花了不少功。花了點時間稍微總結了下相關的技術點,分享給有需要的人。

SurfaceView與TextureView區別

   目前視頻畫面幀的展示控件常用的有兩種 SurfaceView 及TextureView ,這節簡單的介紹 SurfaceView 與 TextureView 區別。

Android圖形渲染

   Android 框架提供了各種用於 2D 和 3D 圖形渲染的 API,可與製造商的圖形驅動程序實現方法交互。

在這裏插入圖片描述
圖1 Surface 如何被渲染

   無論開發者使用什麼渲染 API,一切內容都會渲染到“Surface”。Surface 表示緩衝隊列(BufferQueue)中的生產方,而緩衝隊列通常會被 SurfaceFlinger 消耗。在 Android 平臺上創建的每個窗口都由 Surface 提供支持。所有被渲染的可見 Surface 都被 SurfaceFlinger 合成到顯示部分,也就是說 Surface 對象使應用能夠渲染要在屏幕上顯示的圖像。

SurfaceView

   這小節通過 SurfaceView 概述、SurfaceView 雙緩衝區、SurfaceView 使用、SurfaceView 優缺點等知識點來了解 SurfaceView。

SurfaceView概述

   SurfaceView 是一個組件,其繼承自View,所以它本質也是一個 View 組件,可用於在 View 層次結構中嵌入其他合成層。SurfaceView 採用與其他 View 相同的佈局參數,因此可以像對待其他任何 View 一樣對其進行操作,但 SurfaceView 的內容是透明的。

   但與普通 View 不同的是,它有自己的 Surface,在 WMS 中有對應的 WindowState,在 SurfaceFlinger 中有 Layer, 如下圖。

在這裏插入圖片描述

   也正是如此,雖然在 App 端它仍在 View Hierachy 中,但在 Server 端(WMS 和 SF)中,它與宿主窗口是分離的。這樣的好處是對這個 Surface的渲染可以放到單獨線程去做,渲染時可以有自己的 GL Context,所以它不會影響主線程對事件的響應。

   當有畫面請求時,SurfaceView 通過調用 SurfaceHolder 接口(內部封裝了獲取Surface的接口)將這塊圖形繪衝區的 UI 數據提交給 SurfaceFlinger 服務來處理,SurfaceFlinger 服務可以在合適的時候將該圖形緩衝區合成到屏幕上去顯示,這樣就可以將對應的 SurfaceView 的 UI 展現出來了。

SurfaceView雙緩衝區

   SurfaceView 在更新視圖時用到了兩張 Canvas對應於兩個 Surface,一張 frontCanvas 和一張 backCanvas,每次實際顯示的是 frontCanvas,backCanvas 存儲的是上一次更改前的視圖,當使用 lockCanvas() 獲取畫布時,得到的實際上是 backCanvas 而不是正在顯示的 frontCanvas,之後你在獲取到的 backCanvas 上繪製新視圖,再 unlockCanvasAndPost(Canvas) 此視圖,那麼上傳的這張 Canvas將替換原來的 frontCanvas 作爲新的 frontCanvas,原來的 frontCanvas 將切換到後臺作爲 backCanvas。例如,如果你已經先後兩次繪製了視圖 A 和 B,那麼你再調用 lockCanvas() 獲取視圖,獲得的將是 A 而不是正在顯示的 B,之後你將重繪的 C 視圖上傳,那麼 C 將取代 B 作爲新的 frontCanvas 顯示在 SurfaceView 上,原來的 B 則轉換爲 backCanvas。

SurfaceView優缺點

  • 優點: 使用雙緩衝機制,可以在一個獨立的線程中進行繪製,不會影響主線程,播放視頻時畫面更流暢。
  • 缺點:Surface 不在 View hierachy中,它的顯示也不受 View 的屬性控制,SurfaceView 不能嵌套使用。在 7.0 版本之前不能進行平移,縮放等變換,也不能放在其它 ViewGroup 中,在 7.0 版本之後可以進行平移,縮放等變換。

SurfaceView使用

   一般爲了擴展性,會寫一個子類繼承SurfaceView,這樣有更好的擴展性。SurfaceView 的使用與普通的 View 最大區別是,需要在設置 SurfaceHolder.Callback 來監聽其創建、銷燬、狀態改變,以便在一些場景下恢復畫面,而且視頻畫面通過回調的 SurfaceHolder來綁定指定的MediaPlayer,以下是使用示例。

    ExtentSurfaceView surfaceView = new ExtentSurfaceView(context);
    LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
            LayoutParams.MATCH_PARENT);
    params.gravity = Gravity.CENTER;
    surfaceView.setLayoutParams(params);
    rootView.addView(surfaceView);
    
    public class ExtentSurfaceView extends SurfaceView {

        public ExtentSurfaceView(Context context, AttributeSet attrs,
                                int defStyle) {
            super(context, attrs, defStyle);
            init();
        }

        public ExtentSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }

        public ExtentSurfaceView(Context context) {
            super(context);
            init();
        }

        private void init() {
            getHolder().addCallback(mSHCallback);
        }

        private SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() {
            public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
                Log.i(TAG, "surfaceChanged!!!");
            }

            public void surfaceCreated(SurfaceHolder holder) {
                Log.i(TAG, "surfaceCreated");
                // 設置SurfaceHolder,綁定mediaplayer
                mMediaPlayer.setDisplay(holder);
            }

            public void surfaceDestroyed(SurfaceHolder holder) {
                Log.i(TAG, "surface destroyed!!!");
            }
        };
    }

TextureVeiw

   這小節通過 TextureView 概述、TextureView 使用、TextureView 優缺點等知識點來了解 TextureView。

TextureView概述

   在 4.0(API level 14) 中引入,與 SurfaceView 一樣繼承 View,但它結合了 View 與 SurfaceTexture,所以它可以將內容流直接投影到 View 中,TextureView 重載了 draw() 方法,其中主要工作是 SurfaceTexture 中收到的圖像數據作爲紋理更新到對應的 HardwareLayer 中。

   與 SurfaceView 不同,它不會在 WMS 中單獨創建窗口,而是作爲 View hierachy 中的一個普通 View,因此可以和其它普通 View 一樣進行移動,旋轉,縮放,動畫等變化。值得注意的是 TextureView 必須在硬件加速的窗口中。

TextureView優缺點

  • 優點:支持移動、旋轉、縮放等動畫,支持截圖。
  • 缺點:必須在硬件加速的窗口中使用,佔用內存比 SurfaceView 高,在 5.0 以前在主線程渲染,5.0 以後有單獨的渲染線程,性能相比 SurfaceView 較差些。

TextureView使用

   一般爲了擴展性,會寫一個子類繼承TextureView,這樣有更好的擴展性。TextureView 的使用與普通的 View 最大區別是,需要在設置 TextureView.SurfaceTextureListener 來監聽其內部 SurfaceTexture 創建、銷燬、狀態改變,並且根據回調回來的 SurfaceTexture 創建 Surface 與指定的MediaPlayer綁定,以下是使用示例。

        TextureView textureView = new TextureView(getContext());
        textureView.setSurfaceTextureListener(mSurfaceTextureListener);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        params.gravity = Gravity.CENTER;
        addView(textureView, params);
        
    TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureAvailable" + " width = " + width + " height = " + height);
            Surface surface = new Surface(surfaceTexture);
            //創建surface並綁定mediaplayer
            mMediaPlayer.setSurface(surface);
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureSizeChanged" + " width = " + width + " height = " + height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
            LogUtils.d(TAG, "onSurfaceTextureDestroyed");
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surface) {
            // do nothing
        }
    };

如何選擇?

谷歌官方:在 API 24 及更高版本中,建議實現 SurfaceView 而不是 TextureView。

   根據谷歌官方的建議,api 24及以上的版本使用 SurfaceView 而在其他版本的話可以根據二者的特性選擇對應的控件。下表爲二者正常情況下一些參數的對比情況(僅供參考)。

在這裏插入圖片描述

PlayerBase框架

概述

   PlayerBase是一種將解碼器和播放視圖組件化處理的解決方案框架。您需要什麼解碼器實現框架定義的抽象引入即可,對於視圖,無論是播放器內的控制視圖還是業務視圖,均可以做到組件化處理。將播放器的開發變得清晰簡單,更利於產品的迭代。

   對於PlayerBase其介紹文檔已經很詳細了,這裏不在贅述,有興趣的可以直接看PlayerBase介紹

無縫播放技術介紹

概述

   先上幾個圖。

無縫轉場播放
在這裏插入圖片描述

列表無縫旋轉播放

在這裏插入圖片描述

   從以上圖片的演示可以看出,我們所說的無縫播放技術,其實就是利用 Android 中的動畫實現特定的播放效果,但同時保證播放不中斷的能力。

以無縫轉場播放爲例

   關於Activity等共享元素過渡動畫的介紹,谷歌官方文檔上已經做了比較詳細的介紹,直接看共享元素過渡動畫

   在 PlayerBase 中,過渡動畫實現如下:

Intent intent = new Intent(this, ShareAnimationActivityB.class);
intent.putExtra(ShareAnimationActivityB.KEY_DATA, mData);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
    // 過渡動畫,mLayoutContainer表示共享元素
    ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
            this, mLayoutContainer, "videoShare");
    ActivityCompat.startActivity(this, intent, options.toBundle());
}else{
    startActivity(intent);
}

public class ShareAnimationActivityB extends AppCompatActivity {

    public static final String KEY_DATA = "data_source";

    @BindView(R.id.top_container)
    RelativeLayout mTopContainer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share_animation_b);
        ButterKnife.bind(this);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        DataSource dataSource = (DataSource) getIntent().getSerializableExtra(KEY_DATA);

        DataSource useData = null;
        DataSource playData = ShareAnimationPlayer.get().getDataSource();
        boolean dataChange = playData!=null && !playData.getData().equals(dataSource.getData());
        if(!ShareAnimationPlayer.get().isInPlaybackState() || dataChange){
            useData = dataSource;
        }
       //  動畫結束後,將播放器綁定到新界面上的viewgroup,實現播放。
        ShareAnimationPlayer.get().play(mTopContainer, useData);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ShareAnimationPlayer.get().destroy();
    }
}


   看完過渡動畫,那是如何保證播放的不中斷呢?我們一起來看看。

   以 TextureView 爲例。在前面,我們介紹過,TextureView 其內部是通過封裝 SurfaceTexture 來實現內容呈現的。下面我們再看看其監聽方法。

    private SurfaceTexture mOldSurfaceTexture;
    private TextureView vTextureView;

    TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            // 當TextureView的畫面出現時會回調
            LogUtils.d(TAG, "onSurfaceTextureAvailable" + " width = " + width + " height = " + height);
            if (mOldSurfaceTexture == null) {
                mOldSurfaceTexture = surfaceTexture;
                Surface surface = new Surface(mOldSurfaceTexture);
                //創建surface並綁定mediaplayer
                mMediaPlayer.setSurface(surface);
            } else {
                vTextureView.setSurfaceTexture(mOldSurfaceTexture);
                // 開始播放
                mMediaPlayer.start();
            }

        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureSizeChanged" + " width = " + width + " height = " + height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
            LogUtils.d(TAG, "onSurfaceTextureDestroyed");
            // 當TextureView的畫面丟失時會回調,此時保存原來的畫面
            mOldSurfaceTexture = surface;
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surface) {
            // do nothing
        }
    };

   根據上面的註釋說明,首次綁定 Mediaplayer 時,會將 SurfaceTexture 創建的 Surface 與其綁定,這樣就能正常展示播放內容。而在進行轉場過渡頁面時,播放器會重新綁定到新的父 View 上。此時 SurfaceTexture 會回調 onSurfaceTextureDestroyed 並且把與當前畫面綁定的 SurfaceTexture 回調出來,而再重新綁定到父 View 後又會回調 onSurfaceTextureAvailable。

   正是利用這個實現,PlayerBase 將上一個畫面的 SurfaceTexture 保存,再下一次 onSurfaceTextureAvailable 時重新設置給了 TextureView, 而此時 Mediaplayer 只是暫停而沒有釋放,重新啓動播放就可以實現無縫銜接播放了。

   總結起來,就是不同的渲染視圖使用同一個解碼實例即可。可以簡單比作一個 MediaPlayer 去不斷設置不同的 Surface 呈現播放,但這個過程如果自己實現就比較複雜。PlayerBase 中的 RelationAssist 就是爲了簡化這個過程而設計的,在不同的頁面或視圖切換播放時,只需要提供並傳入對應位置的視圖容器(ViewGroup類型)即可。內部複雜的設置項和關聯由 RelationAssist 完成。

幾個無縫轉場實現方案

Activity

   PlayerBase 中的無縫轉場是使用的 Activity 的實現方式,該方法實現較爲方便,但存在如下問題:

  1. 受限於Activity生命週期,可能會有耗時影響;
  2. 共享動畫能力支持弱;
  3. 堆棧管理弱。

Fragment

   愛奇藝,好看等視頻軟件的列表播放是使用 Fragment 來實現共享元素跳轉,Fragment的實現成本可能相對較高,比Activity靈活,但動畫能力也比較弱。

View

   西瓜視頻的實現是通過 View 來實現的,他們提供了一個開源的頁面導航和組合框架,方法實現這種複雜的過渡動畫等,有興趣的可以去看看相關文檔。

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章