1. 程式人生 > >一步步自定義視訊播放器——使用SurfaceView播放視訊

一步步自定義視訊播放器——使用SurfaceView播放視訊

Surface

Surface與SurfaceView這篇文章對Surface和SurfaceView做了很詳細的解釋。

簡單的說Surface對應了一塊螢幕緩衝區,每個window對應一個Surface,任何View都要畫在Surface的Canvas上。傳統的view共享一塊螢幕緩衝區,所有的繪製必須在UI執行緒中進行。

Surface可以理解為: Surface類似一個控制代碼,可以得到Canvas、原始緩衝區以及其他方面的內容。
Canvas用於畫圖
原始緩衝區用於儲存當前視窗的畫素資料

SurfaceView

SurfaceView繼承自View,但它有自己的Surface。

 if (mWindow == null) {    
         mWindow = new MyWindow(this);    
         mLayout.type = mWindowType;    
         mLayout.gravity = Gravity.LEFT|Gravity.TOP;    
         mSession.addWithoutInputChannel(mWindow, mWindow.mSeq, mLayout,    
         mVisible ? VISIBLE : GONE, mContentInsets);     
}

源碼錶明,SurfaceView會自己建立一個MyWindow,一個window對應一個Surface,因此SurfaceView也就內嵌了一個自己的Surface。

傳統View及其派生類的更新只能在UI執行緒,然而UI執行緒還同時處理其他互動邏輯,這就無法保證View更新的速度和幀率了,而SurfaceView可以用獨立的執行緒進行繪製,因此可以提供更高的幀率,例如遊戲,攝像頭取景等場景就比較適合SurfaceView來實現。

SurfaceHolder

SurfaceHolder是一個介面,其作用就像一個關於Surface的監聽器,提供訪問和控制SurfaceView內嵌的Surface相關的方法。
它通過三個回撥方法,讓我們可以感知到Surface的建立、銷燬或者改變。
在SurfaceView中有一個方法getHolder,可以很方便地獲得SurfaceView內嵌的Surface所對應的監聽器介面SurfaceHolder。
SurfaceHolder.Callback主要是當底層的Surface被建立、銷燬或者改變時提供回撥通知,由於繪製必須在Surface被建立後才能進行,因此SurfaceHolder.Callback中的surfaceCreated 和surfaceDestroyed 就成了繪圖處理程式碼的邊界。
SurfaceHolder.Callback中定義了三個介面方法:

  1. abstract void surfaceChanged(SurfaceHolder holder, int format, int width, int height):當surface發生任何結構性的變化時(格式或者大小),該方法就會被立即呼叫。
  2. abstract void surfaceCreated(SurfaceHolder holder):當surface物件建立後,該方法就會被立即呼叫。
  3. abstract void surfaceDestroyed(SurfaceHolder holder):當surface物件在將要銷燬前,該方法會被立即呼叫。

小結

  • SurfaceView是一個擁有獨立繪圖層的特殊View
  • Surface是記憶體中的一段繪圖緩衝區
  • SurfaceView中具有兩個Surface, 即雙緩衝機制
  • SurfaceHolder是Surface的持有者,SurfaceView就是通過過SurfaceHolder來對Surface進行管理控制的。並且SurfaceView.getHolder方法可以獲取SurfaceView相應的SurfaceHolder。
  • Surface是在SurfaceView所在的Window可見的時候建立的。我們可以使用SurfaceHolder.addCallback方法來監聽Surface的建立與銷燬的事件。

從設計模式的高度來看,Surface、SurfaceView和SurfaceHolder實質上就是廣為人知的MVC,即Model-View-Controller。
Model就是模型的意思,或者說是資料模型,或者更簡單地說就是資料,也就是這裡的Surface;
View即檢視,代表使用者互動介面,也就是這裡的SurfaceView;
SurfaceHolder很明顯可以理解為MVC中的Controller(控制器)。

SurfaceView與MediaPlayer結合

SurfaceView解析視訊的流程:首先確定視訊的格式,知道編碼格式後,通過編碼格式進行解碼,得到一幀一幀的影象,並把這些影象快速的顯示在介面上。
簡單地說,SurfaceView用來顯示MediaPlayer中解析得到的視訊影象。
MediaPlayer通過setDisplay(SurfaceHolder sh)來指定SurfaceView顯示影象。

SurfaceView雙緩衝

SurfaceView和大部分視訊應用一樣,把視訊解析成的一幀幀影象進行顯示。如果把這個解析放在一個執行緒中完成,可能在上一幀顯示後,下一幀來不及解析,導致畫面不流暢或者視訊不同步。通過雙緩衝機制來顯示影象能夠有效解決上述問題。
雙緩衝機制可以理解為兩個執行緒輪番解析視訊流的幀影象,當一個執行緒解析完幀影象後,把影象渲染到介面中,同時另外一個執行緒開始解析下一幀影象,使得兩個執行緒輪番配合區解析視訊流,達到流暢播放的效果。

注意

 SurfaceView內部實現了雙緩衝的機制,但是實現這個功能是非常消耗系統記憶體的。因為移動裝置的侷限性,Android在設計的時候規定,SurfaceView如果為使用者可見的時候,建立SurfaceView的SurfaceHolder,如果發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。

如果開發人員不對SurfaceHolder進行維護,會出現最小化程式後,再開啟應用的時候,視訊的聲音在繼續播放,但是不顯示畫面了的情況,這就是因為當SurfaceView不被使用者可見的時候,之前的SurfaceHolder已經被銷燬了,再次進入的時候,介面上的SurfaceHolder已經是新的SurfaceHolder了。所以SurfaceHolder需要我們開發人員去編碼維護,維護SurfaceHolder需要用到上述的三個回撥方法。

小例子

使用的時候注意要配置許可權

    <uses-permission android:name="android.permission.INTERNET" />

使用步驟為:
1.建立 MediaPlayer,並讓他載入指定的視訊檔案
2.在佈局中定義SurfaceView元件,或者在程式中建立SurfaceView元件
3.為SurfaceView的SurfaceHolder新增Callback監聽器
4.呼叫MediaPlayer物件的setDisplay(SurfaceHolder sh)將所播放的視訊影象輸出到指定的SurfaceView元件中
5.呼叫MediaPlayer物件的start(),stop(),和pause()方法。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化
        mediaPlayer = new MediaPlayer();
        surfaceView = findViewById(R.id.surface_view);
        //設定螢幕常亮
        surfaceView.getHolder().setKeepScreenOn(true);
        //添加回調介面
        surfaceView.getHolder().addCallback(callback);
    }

     public void play() {
        try {
            mediaPlayer.reset();//重置
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.setDataSource(URL);//設定播放路徑 
            mediaPlayer.setDisplay(surfaceView.getHolder());//視訊輸出到SurfaceView上
            mediaPlayer.prepare();//使用同步方式
            mediaPlayer.start();//開始播放

        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(MainActivity.this, "路徑錯誤", Toast.LENGTH_SHORT).show();
        }
    }

完整程式碼

package com.example.com.myapplication;

import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Toast;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {
    private SurfaceView surfaceView;
    private MediaPlayer mediaPlayer;
    private String URL = "http://fairee.vicp.net:83/2016rm/0116/baishi160116.mp4";
    private int position;//記錄位置

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化
        mediaPlayer = new MediaPlayer();
        surfaceView = findViewById(R.id.surface_view);
        //設定螢幕常亮
        surfaceView.getHolder().setKeepScreenOn(true);
        //添加回調介面
        surfaceView.getHolder().addCallback(callback);
    }


    private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {

        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                position = mediaPlayer.getCurrentPosition();
                mediaPlayer.stop();
            }

        }
    };

    public void play() {
        try {
            mediaPlayer.reset();//重置
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            //raw資料夾下面的內容
//            Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.baishi);
//            mediaPlayer.setDataSource(this, uri);
            mediaPlayer.setDisplay(surfaceView.getHolder());
            mediaPlayer.setDataSource(URL);
            //視訊輸出到SurfaceView上

            mediaPlayer.prepare();//使用同步方式
            mediaPlayer.start();//開始播放

        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(MainActivity.this, "路徑錯誤", Toast.LENGTH_SHORT).show();
        }
    }

    public void start(View view) {
        play();
    }

    public void pause(View view) {
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            position = mediaPlayer.getCurrentPosition();
        }
    }

    public void goOn(View view) {
        mediaPlayer.seekTo(position);
        mediaPlayer.start();
    }

    public void stop(View view) {
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.stop();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            position = mediaPlayer.getCurrentPosition();

        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //釋放資源
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.stop();
            mediaPlayer.release();
        }
    }
}

佈局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.com.myapplication.MainActivity">

    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="400dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="start"
            android:text="播放" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="pause"
            android:text="暫停" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="goOn"
            android:text="繼續" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="stop"
            android:text="停止" />
    </LinearLayout>
</LinearLayout>

實現效果:
這裡寫圖片描述