Android SurfaceView + MediaPlayer實現分段視頻無縫播放

Android當中實現視頻播放的方式有兩種,即:經過VideoView實現或者經過SurfaceView + MediaPlayer實現。android

由淺至深,首先來看下想要在Android上播放一段視頻,咱們應當怎麼作。web

前面咱們已經提到了兩種方式,這裏咱們來看一下具備更好的拓展性的第二種方式,也就是經過SurfaceView + MediaPlayer進行實現。緩存

首先,咱們來定義一個佈局文件以下,爲了方便起見,咱們僅僅只在該佈局中定義了一個SurfaceView:app

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:id="@+id/videoLayout" >

    <SurfaceView
        android:id="@+id/surface"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="center">
    </SurfaceView>

</FrameLayout>

接着就是Activity類文件的定義:ide

package com.example.videodemo;

import android.app.Activity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class VideoPlayActivity extends Activity implements
        SurfaceHolder.Callback {
    /** Called when the activity is first created. */
    MediaPlayer player;
    SurfaceView surface;
    SurfaceHolder surfaceHolder;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_play);

        initView();
    }

    private void initView() {
        surface = (SurfaceView) findViewById(R.id.surface);
        surfaceHolder = surface.getHolder(); // SurfaceHolder是SurfaceView的控制接口
        surfaceHolder.addCallback(this); // 由於這個類實現了SurfaceHolder.Callback接口,因此回調參數直接this
    }

    @Override
    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder arg0) {
        // 必須在surface建立後才能初始化MediaPlayer,不然不會顯示圖像
        player = new MediaPlayer();
        player.setAudioStreamType(AudioManager.STREAM_MUSIC);
        player.setDisplay(surfaceHolder);
        // 設置顯示視頻顯示在SurfaceView上
        try {
            player.setDataSource("你要播放的視頻的url");
            player.prepare();
            player.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        if (player.isPlaying()) {
            player.stop();
        }
        player.release();
        // Activity銷燬時中止播放,釋放資源。不作這個操做,即便退出仍是能聽到視頻播放的聲音
    }
}

由此你能夠看到,這種實現方式有幾點值得注意的地方是:佈局

一、你須要一個媒體播放器對象"MediaPlayer",該對象會負責播放你指定的視頻。測試

二、若是說MediaPlayer負責播放視頻,那麼咱們剛剛定義的SurfaceView則用於在屏幕中顯示播放視頻。this

  (因此又能夠理解爲,若是MediaPlayer是一副畫,而SurfaceView則是讓這幅畫呈如今人們眼前的畫紙)url

三、MediaPlayer類的成員方法設置用於顯示媒體視頻的SurfaceHolder,正如上面所說,就如同你擇不一樣的畫紙來呈現你的畫。spa

四、MediaPlayer類的成員方法setDataSource用於指定你要播放的視頻數據源。

五、僅僅是設置完數據源是不足夠的,設置完數據源和顯示的Surface後,你須要調用prepare()或prepareAsync()來讓你的視頻數據源stand by..

六、因此你也可能已經發現,對於一段視頻的播放,MediaPlayer是關鍵,關於該類的更多使用,這篇博客裏有更詳細的說明:Android - MediaPlayer類的使用說明

由此咱們已經基本掌握了,在android端簡單的播放視頻的方法。一切看上去十分美好。

但作開發就是有這麼蛋疼,maybe有不少時候爲了加快video與server端之間上傳於下載的速率,有時候會對視頻作分段處理。

正如同作web開發時,上傳和下載文件時,若是文件過大,不少時候咱們會選擇對文件作「切割處理同樣」。

那麼這個時候,就出現了一種狀況,就是可能你要播放的一段視頻,

事實上是由幾小段視頻組合而成的。因此就涉及到了連續播放。

可能當面對到這樣的需求時,咱們首先最容易想到的就是:

對每段視頻進行監聽,當監聽到它播放結束時,馬上作Refresh切換到下一段視頻分段的播放。

而MediaPlayer的確也提供了這樣的監聽事件,正是:MediaPlayer.OnCompletionListener()。

我在網上查閱相關實現的功能時,也只看到相似的說法,也就是說在該監聽內作實現:

當一段數據源播放完畢後,執行player.reset()釋放數據源,而後再設置新的資源進行播放。

但這樣作有很大的一個弊端就是,reset掉舊的數據源以後,新的數據源會有一段「加載時間」。

也就是說,在這段時間內,用戶看到的播放界面就處於一個停頓狀態。

那麼,爲了最大化的避免這個所謂的「停頓時間」,又應該怎麼去作呢?

首先考慮到的即是,在一段視頻開始播放的同時,便開始作第二段視頻播放的「準備工做」。

可是經過前面的例子咱們之前看到了,基於MediaPlayer自己的特性和限制。

若是咱們想要實現這樣的方式,那麼單一的MediaPlayer是知足不了咱們的需求的。

因此咱們要作的工做即是:當咱們進入視頻播放界面,第一段視頻準備完畢,開始播放後,

便開始着手初始化另外一個新的MediaPlayer,這個新的MediaPlayer的數據源固然是接下來要播放的下一段視頻的url!

當這個MediaPlayer對象的準備工做都搞定後,剩下的工做就是:

咱們須要「一顆釘子」,來將兩個分段的視頻段鏈接起來。

而這個釘子就是Android r16後添加的一個方法:setNextMediaPlayer()方法。

關於這個方法的使用,我找了又找,終於在一篇文章裏,看到了一個這樣簡短的說明:

在第一個MediaPlayer類執行結束前的任什麼時候間調用setNextMediaPlayer(MediaPlayernext)這個方法,

該方法的參數是第二個文件建立的MediaPlayer實例。而後Android系統將會在您第一個中止的時候緊接着播放第二個文件。

但我認爲,在這個說明裏,你應該注意到的關鍵點是:第一個MediaPlayer類執行結束前的任什麼時候間調用這個方法。

也就是說,你必須在前一個MediaPlayer對象播放完畢以前使用該方法。

例如我後來發現,若是理想的在咱們前面提到的OnCompletionListener監聽中使用該方法,是無效的。

而且,彷佛並不如該說明而言的「Android系統將會在您第一個中止的時候緊接着播放第二個文件」。

也就是說,這個切換播放的動做不是自動的,還須要咱們手動的作一個小的控制,立刻接下來就會說到。

到了這裏,咱們要實現的思路已經很明確了:在一段視頻播放的同時,作下一段視頻的player的初始化準備工做。

而此時另外一個格外須要記住的就是:不要再在UI線程去開啓新的MediaPlayer的賦值工做.

原理很簡單,其實也是Android開發所必須記住的,便是永遠不要在UI線程裏去作耗時的操做。

這樣作的後果基本有幾種,一種是報告「在主線程作了太多操做」的異常,而另外也可能出現,屏幕響應遲緩,

也就是說,例如你的視頻播放界面可能還存在一些按鈕和響應事件之類,這個響應會出現延遲。最後,固然也極可能出現ANR。

因此,咱們還須要作的工做就是,將其它負責後續播放的MediaPlayer對象的初始化與賦值工做放在新的線程裏去執行。

而最後咱們須要作的,則是在OnCompletionListener裏進行監聽,當一段視頻播放完畢後,

立刻執行mp.setDisplay(null),而後調用負責下一個視頻分段播放的MediaPlayer執行setDisplay(surfaceHolder)。

說了這麼多,仍是經過代碼說話吧:

@SuppressLint("NewApi")
public class MainActivity extends Activity implements SurfaceHolder.Callback {
    //用於播放視頻的mediaPlayer對象
    private MediaPlayer firstPlayer,     //負責播放進入視頻播放界面後的第一段視頻
                        nextMediaPlayer, //負責一段視頻播放結束後,播放下一段視頻
                        cachePlayer,     //負責setNextMediaPlayer的player緩存對象
                        currentPlayer;   //負責當前播放視頻段落的player對象
    //負責配合mediaPlayer顯示視頻圖像播放的surfaceView
    private SurfaceView surface;
    private SurfaceHolder surfaceHolder;
    //底部聊天欄
    private LinearLayout bottom_bar_layout;
    private FrameLayout video_layout;
    
    //================================================================
    
    //存放全部視頻端的url
    private ArrayList<String> VideoListQueue = new ArrayList<String>();
    //全部player對象的緩存
    private HashMap<String, MediaPlayer> playersCache = new HashMap<String, MediaPlayer>();
    //當前播放到的視頻段落數
    private int currentVideoIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //橫屏顯示
        this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        //初始化界面控件
        initView();
    }

    /*
     * 負責界面銷燬時,release各個mediaplayer
     * @see android.app.Activity#onDestroy()
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (firstPlayer != null) {
            if (firstPlayer.isPlaying()) {
                firstPlayer.stop();
            }
            firstPlayer.release();
        }
        if (nextMediaPlayer != null) {
            if (nextMediaPlayer.isPlaying()) {
                nextMediaPlayer.stop();
            }
            nextMediaPlayer.release();
        }

        if (currentPlayer != null) {
            if (currentPlayer.isPlaying()) {
                currentPlayer.stop();
            }
            currentPlayer.release();
        }
        currentPlayer = null;
    }

    /*
     * 界面控件的初始化
     */
    private void initView() {
        surface = (SurfaceView) findViewById(R.id.surface);

        surfaceHolder = surface.getHolder();// SurfaceHolder是SurfaceView的控制接口
        surfaceHolder.addCallback(this); // 由於這個類實現了SurfaceHolder.Callback接口,因此回調參數直接this

        bottom_bar_layout = (LinearLayout) findViewById(R.id.live_buttom_bar);
        
        //點擊屏幕任何地點,控制底部聊天欄的隱藏或顯示
        video_layout = (FrameLayout) findViewById(R.id.videoLayout);
        video_layout.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View arg0) {
                if (bottom_bar_layout.getVisibility() == View.VISIBLE) {
                    bottom_bar_layout.setVisibility(View.GONE);
                } else {
                    bottom_bar_layout.setVisibility(View.VISIBLE);
                }

            }
        });
    }

    @Override
    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
        // TODO 自動生成的方法存根

    }

    @Override
    public void surfaceCreated(SurfaceHolder arg0) {
        //surfaceView建立完畢後,首先獲取該直播間全部視頻分段的url
        getVideoUrls();
        //而後初始化播放手段視頻的player對象
        initFirstPlayer();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder arg0) {
        // TODO 自動生成的方法存根

    }

    /*
     * 初始化播放首段視頻的player
     */
    private void initFirstPlayer() {
        firstPlayer = new MediaPlayer();
        firstPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        firstPlayer.setDisplay(surfaceHolder);

        firstPlayer
                .setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mp) {
                        onVideoPlayCompleted(mp);
                    }
                });

        //設置cachePlayer爲該player對象
        cachePlayer = firstPlayer;
        initNexttPlayer();

        //player對象初始化完成後,開啓播放
        startPlayFirstVideo();
    }

    private void startPlayFirstVideo() {
        try {
            firstPlayer.setDataSource(VideoListQueue.get(currentVideoIndex));
            firstPlayer.prepare();
            firstPlayer.start();
        } catch (IOException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }
    }

    /*
     * 新開線程負責初始化負責播放剩餘視頻分段的player對象,避免UI線程作過多耗時操做
     */
    private void initNexttPlayer() {
        new Thread(new Runnable() {

            @Override
            public void run() {

                for (int i = 1; i < VideoListQueue.size(); i++) {
                    nextMediaPlayer = new MediaPlayer();
                    nextMediaPlayer
                            .setAudioStreamType(AudioManager.STREAM_MUSIC);

                    nextMediaPlayer
                            .setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                                @Override
                                public void onCompletion(MediaPlayer mp) {
                                    onVideoPlayCompleted(mp);
                                }
                            });

                    try {
                        nextMediaPlayer.setDataSource(VideoListQueue.get(i));
                        nextMediaPlayer.prepare();
                    } catch (IOException e) {
                        // TODO 自動生成的 catch 塊
                        e.printStackTrace();
                    }

                    //set next mediaplayer
                    cachePlayer.setNextMediaPlayer(nextMediaPlayer);
                    //set new cachePlayer
                    cachePlayer = nextMediaPlayer;
                    //put nextMediaPlayer in cache
                    playersCache.put(String.valueOf(i), nextMediaPlayer);

                }

            }
        }).start();
    }

    /*
     * 負責處理一段視頻播放事後,切換player播放下一段視頻
     */
    private void onVideoPlayCompleted(MediaPlayer mp) {
        mp.setDisplay(null);
        //get next player
        currentPlayer = playersCache.get(String.valueOf(++currentVideoIndex));
        if (currentPlayer != null) {
            currentPlayer.setDisplay(surfaceHolder);
        } else {
            Toast.makeText(MainActivity.this, "視頻播放完畢..", Toast.LENGTH_SHORT)
                    .show();
        }
    }
    
    private void getVideoUrls() {
        for (int i = 0; i < 5; i++) {
            String url = getURI(i);
            VideoListQueue.add(url);
        }
    }

    private String getURI(int index) {
        return "要播放的第"+index+"段視頻的URI";
    }

}

而最後額外說明的就是,在上面的代碼中,我選擇新開線程直接根據總的視頻段數,循環完成全部視頻段的MediaPlayer對象的初始化與賦值工做。

其實原本另一種實現方式彷佛也很不錯,便是在前一個MediaPlayer對象的OnInfoListener中進行下一個視頻段MediaPlayer的初始化工做。

也就是說,當前一段視頻開始或結束緩衝時,纔開啓它以後的一段視頻段的初始化工做。但屢次測試後,發現:

這種實現方式,若是你這次的播放中,視頻分段的數量較多時,總會出現一些莫名其妙的異常,也沒能太弄清楚是什麼緣由形成的。

因此總的來講,仍是能夠根據實際狀況來選擇更合適的方式。

相關文章
相關標籤/搜索