MediaPlayer詳解和使用

Android多媒體相關的API,網上基本都能找到很多相關的文章,使用起來也很簡單,一直在猶豫要不要寫這方面的內容,後來決定還是寫一寫,一方面算是一個歸納總結,另一方面,也方便以後查閱。這一篇就寫一下MediaPlayer。

狀態圖詳解

下圖是一個MediaPlayer的生命週期和狀態。其中,橢圓代表MediaPlayer可能駐留的狀態,弧線表示MediaPlayer的播放控制操作。這裏有兩種類型的弧線,單箭頭弧線代表同步方法調用,雙箭頭弧線代表異步方法調用。
這裏寫圖片描述

1、新創建的MediaPlayer對象、或者調用了reset()方法的MediaPlayer對象,都處於Idle狀態,這兩種方法得到的對象,有一個微小但十分重要的差別。

處於Idle狀態時,調用 getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(), setLooping(), setVolume(), pause(), start(), stop(), seekTo(), prepare(), prepareAsync()方法都會報錯。新創建的MediaPlayer對象,調用以上方法,無法接收到註冊的OnErrorListener.onError()回調;調用reset()方法的MediaPlayer對象可以接收到回調。

MediaPlayer不再被使用時,應立即調用release()方法來釋放資源,資源可能包括硬件加速組件的單態固件,若沒有調用release()方法可能會導致之後的MediaPlayer對象實例無法使用這種單態硬件資源,導致異常。

一旦MediaPlayer對象進入了End狀態,將不能再被使用,也沒有辦法再遷移到其他狀態。

2、由於種種原因,一些操作可能會失敗,如不支持的格式/分辨率太高/流超時等,還有編程錯誤(比如在無效狀態下調用某個操作),此時會回調OnErrorListener.onError()方法(需客戶端提前註冊listener)。一旦發生錯誤,MediaPlayer對象會進入Error狀態,此時可以調用reset()方法把這個對象恢復到Idle狀態。

在不合法的狀態下調用一些方法,如prepare()、prepareAsync()和setDataSource()等會拋出ILlegalStateException異常。

3、Idle狀態下,調用 setDataSource()方法會遷移到Initialized狀態,非Idle狀態下調用此方法會報 ILlegalStateException異常。注意,setDataSource()方法可能會拋出IOException異常。

4、調用prepare()、prepareAsync()方法可以遷移到Prepared狀態,該狀態下纔可以進行基本播放操作。

異步的prepareAsync()方法需要通過OnPrepareListener.onPrepared()監聽準備是否完成,Preparing是一箇中間狀態,如果在此狀態下調用任何影響播放功能的方法,最終的運行結果都是未知的。

在不合適的狀態下調用prepare()和prepareAsync()方法會拋出ILlegalStateException異常。

5、調用start()方法成功返回後,會遷移到Started狀態,isPlaying()方法返回是否處於Started狀態。遷移到Started狀態時,可以通過OnBufferingUpdateListener.onBufferingUpdate()回調得知。

Started狀態下調用start()方法沒有影響。

6、調用pause()方法並返回時,會遷移到Paused狀態。注意,Started與Paused狀態的轉換在內部的播放引擎中是異步的,所以isPlaying()可能會延時更新,如果是播放網絡流媒體,這個延時可能會有幾秒。

Paused狀態下調用pause()方法沒有影響。

7、除了Idle、Initialized狀態,其它狀態下都可以調用stop()遷移到Stopped狀態,Stopped狀態下調用stop()方法沒有影響。

8、seekTo()方法可以調整播放位置,seekTo()方法是異步的,尤其是播放網絡流媒體時延時很明顯。實際定位完成後,通過OnSeekComplete.onSeekComplete()通知。

“活動狀態”(Prepared、Started、Paused、PlaybackCompleted狀態)下都可以調用seekTo()方法。

9、遷移到PlaybackCompleted狀態後,如果通過setLooping()方法開啓了循環模式,會重新進入到Started狀態,並且不會回調OnCompletion.onCompletion()方法、如果沒有開啓循環,就會回調這個方法。

PlaybackCompleted狀態下調用start()方法會遷移到Started狀態。

各方法的調用狀態

除了下面幾個方法調用時需要特別注意狀態的判斷,其餘常用方法,基本所有狀態都是OK的,或者即便狀態不對也不會報錯。如果對某個方法調用有疑問,查閱API文檔,下面只列出一些常用的、需要注意狀態的方法。

下面這幾個方法需要注意下:

1、setDataSource()
有效狀態:Idle
調用結果:調用成功,會遷移到Initialized狀態
無效狀態:報IllegalStateException異常

2、prepare()、prepareAsync()
有效狀態:Initialized/Stopped
調用結果:調用成功,會遷移到Prepared/Preparing狀態
無效狀態:報IllegalStateException異常

3、pause()
有效狀態:Started/Paused
調用結果:調用成功,會遷移到Paused狀態
無效狀態:player進入Error狀態

4、start()
有效狀態:Prepared/Started/Paused/PlaybackCompleted
調用結果:調用成功,會遷移到Started狀態
無效狀態:player進入Error狀態

5、stop()
有效狀態:Prepared/Started/Stopped/Paused/PlaybackCompleted
調用結果:調用成功,會遷移到Stopped狀態
無效狀態:player進入Error狀態

6、seekTo()
有效狀態:Prepared/Started/Paused/PlaybackCompleted
調用結果:調用成功,不會改變player的狀態
無效狀態:player進入Error狀態

使用demo

使用之前先整理一下大概要用到播放操作的方法,每個方法的有效狀態是怎樣。

大概要用到setDataSource()、prepare()、start()、pause()、seekTo()這5個方法,stop()方法一般不用,不播放的時候最好調release()釋放資源。其中setDataSource()和prepare()基本就是初始化的時候連續調用,不太需要注意狀態,所以剩下需要注意的也就start()、pause()和seekTo()這3個方法。

排除stop()方法和Stopped狀態後,從“不可操作”到“可操作”的分界點就在Prepared狀態,所以我們可以使用一個變量hasPrepared來標記是否可操作,start()和seekTo()方法在“可操作”狀態下都是可以正常調用的。

剩下的就是pause()方法,只能在Started和Paused狀態下調用,可以使用一個變量canPause來標記是否可以調用pause()方法。實際上,Prepared和PlaybackCompleted狀態在程序中也基本是個瞬時狀態,基本不會停留。一旦Prepared,程序就會調用start()方法進行播放;一旦PlaybackCompleted,就會進行下一個曲目的初始化、準備、播放。

那麼下面就看一下使用方法吧:

public class MyPlayer implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
    private MediaPlayer mPlayer;
    private boolean hasPrepared;

    private void initIfNecessary() {
        if (null == mPlayer) {
            mPlayer = new MediaPlayer();
            mPlayer.setOnErrorListener(this);
            mPlayer.setOnCompletionListener(this);
            mPlayer.setOnPreparedListener(this);
        }
    }

    public void play(Context context, Uri dataSource) {
        hasPrepared = false; // 開始播放前講Flag置爲不可操作
        initIfNecessary(); // 如果是第一次播放/player已經釋放了,就會重新創建、初始化
        try {
            mPlayer.reset();
            mPlayer.setDataSource(context, dataSource); // 設置曲目資源
            mPlayer.prepareAsync(); // 異步的準備方法
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        // release()會釋放player、將player置空,所以這裏需要判斷一下
        if (null != mPlayer && hasPrepared) {
            mPlayer.start();
        }
    }

    public void pause() {
        if (null != mPlayer && hasPrepared) {
            mPlayer.pause();
        }
    }

    public void seekTo(int position) {
        if (null != mPlayer && hasPrepared) {
            mPlayer.seekTo(position);
        }
    }

    // 對於播放視頻來說,通過設置SurfaceHolder來設置顯示Surface。這個方法不需要判斷狀態、也不會改變player狀態
    public void setDisplay(SurfaceHolder holder) {
        if (null != mPlayer) {
            mPlayer.setDisplay(holder);
        }
    }
    public void release() {
        hasPrepared = false;
        mPlayer.stop();
        mPlayer.release();
        mPlayer = null;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        hasPrepared = true; // 準備完成後回調到這裏
        start();
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        hasPrepared = false;
        // 通知調用處,調用play()方法進行下一個曲目的播放
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        hasPrepared = false;
        return false;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章