用MediaPlayer+TextureView封裝一個完美實現全屏、小窗口的視頻播放器

項目已添加IjkPlayer支持,後續逐漸完善其餘功能。
地址:github.com/xiaoyanger0…html

爲何使用TextureView

在Android總播放視頻能夠直接使用VideoViewVideoView是經過繼承自SurfaceView來實現的。SurfaceView的大概原理就是在現有View的位置上建立一個新的Window,內容的顯示和渲染都在新的Window中。這使得SurfaceView的繪製和刷新能夠在單獨的線程中進行,從而大大提升效率。可是呢,因爲SurfaceView的內容沒有顯示在View中而是顯示在新建的Window中, 使得SurfaceView的顯示不受View的屬性控制,不能進行平移,縮放等變換,也不能放在其它RecyclerViewScrollView中,一些View中的特性也沒法使用。android

TextureView是在4.0(API level 14)引入的,與SurfaceView相比,它不會建立新的窗口來顯示內容。它是將內容流直接投放到View中,而且能夠和其它普通View同樣進行移動,旋轉,縮放,動畫等變化。TextureView必須在硬件加速的窗口中使用。git

TextureView被建立後不能直接使用,必需要在它被它添加到ViewGroup後,待SurfaceTexture準備就緒才能起做用(看TextureView的源碼,TextureView是在繪製的時候建立的內部SurfaceTexture)。一般須要給TextureView設置監聽器SurfaceTextuListenergithub

mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture準備就緒
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture緩衝大小變化
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // SurfaceTexture即將被銷燬
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // SurfaceTexture經過updateImage更新
    }
});複製代碼

SurfaceTexture的準備就緒、大小變化、銷燬、更新等狀態變化時都會回調相對應的方法。當TextureView內部建立好SurfaceTexture後,在監聽器的onSurfaceTextureAvailable方法中,用SurfaceTexture來關聯MediaPlayer,做爲播放視頻的圖像數據來源。網絡

SurfaceTexture做爲數據通道,把從數據源(MediaPlayer)中獲取到的圖像幀數據轉爲GL外部紋理,交給TextureVeiw做爲View heirachy中的一個硬件加速層來顯示,從而實現視頻播放功能。異步

MediaPlayer介紹

MediaPlayer是Android原生的多媒體播放器,能夠用它來實現本地或者在線音視頻的播放,同時它支持https和rtspide

MediaPlayer定義了各類狀態,能夠理解爲是它的生命週期。oop

MediaPlayer狀態圖(生命週期)
MediaPlayer狀態圖(生命週期)

這個狀態圖描述了MediaPlayer的各類狀態,以及主要方法調用後的狀態變化。動畫

MediaPlayer的相關方法及監聽接口:ui

方法 介紹 狀態
setDataSource 設置數據源 Initialized
prepare 準備播放,同步 Preparing —> Prepared
prepareAsync 準備播放,異步 Preparing —> Prepared
start 開始或恢復播放 Started
pause 暫停 Paused
stop 中止 Stopped
seekTo 到指定時間點位置 PrePared/Started
reset 重置播放器 Idle
setAudioStreamType 設置音頻流類型 --
setDisplay 設置播放視頻的Surface --
setVolume 設置聲音 --
getBufferPercentage 獲取緩衝半分比 --
getCurrentPosition 獲取當前播放位置 --
getDuration 獲取播放文件總時間 --
內部回調接口 介紹 狀態
OnPreparedListener 準備監聽 Preparing ——>Prepared
OnVideoSizeChangedListener 視頻尺寸變化監聽 --
OnInfoListener 指示信息和警告信息監聽 --
OnCompletionListener 播放完成監聽 PlaybackCompleted
OnErrorListener 播放錯誤監聽 Error
OnBufferingUpdateListener 緩衝更新監聽 --

MediaPlayer在直接new出來以後就進入了Idle狀態,此時能夠調用多個重載的setDataSource()方法從idle狀態進入Initialized狀態(若是調用setDataSource()方法的時候,MediaPlayer對象不是出於Idle狀態,會拋異常,能夠調用reset()方法回到Idle狀態)。

調用prepared()方法和preparedAsync()方法進入Prepared狀態,prepared()方法直接進入Parpared狀態,preparedAsync()方法會先進入PreParing狀態,播放引擎準備完畢後會經過OnPreparedListener.onPrepared()回調方法通知Prepared狀態。

在Prepared狀態下就能夠調用start()方法進行播放了,此時進入started()狀態,若是播放的是網絡資源,Started狀態下也會自動調用客戶端註冊的OnBufferingUpdateListener.OnBufferingUpdate()回調方法,對流播放緩衝的狀態進行追蹤。

pause()方法和start()方法是對應的,調用pause()方法會進入Paused狀態,調用start()方法從新進入Started狀態,繼續播放。

stop()方法會使MdiaPlayer從Started、Paused、Prepared、PlaybackCompleted等狀態進入到Stoped狀態,播放中止。

當資源播放完畢時,若是調用了setLooping(boolean)方法,會自動進入Started狀態從新播放,若是沒有調用則會自動調用客戶端播放器註冊的OnCompletionListener.OnCompletion()方法,此時MediaPlayer進入PlaybackCompleted狀態,在此狀態裏能夠調用start()方法從新進入Started狀態。

封裝考慮

MediaPlayer的方法和接口比較多,不一樣的狀態調用各個方法後狀態變化狀況也比較複雜。播放相關的邏輯只與MediaPlayer的播放狀態和調用方法相關,而界面展現和UI操做不少時候都須要根據本身項目來定製。參考原生的VideoView,爲了解耦和方便定製,把MediaPlayer的播放邏輯和UI界面展現及操做相關的邏輯分離。我是把MediaPlayer直接封裝到NiceVideoPlayer中,各類UI狀態和操做反饋都封裝到NiceVideoPlayerController裏面。若是須要根據不一樣的項目需求來修改播放器的功能,就只重寫NiceVideoPlayerController就能夠了。

NiceVideoPlayer

首先,須要一個FrameLayout容器mContainer,裏面有兩層內容,第一層就是展現播放視頻內容的TextureView,第二層就是播放器控制器mController。那麼自定義一個NiceVideoPlayer繼承自FrameLayout,將mContainer添加到當前控件:

public class NiceVideoPlayer extends FrameLayout{

    private Context mContext;
    private NiceVideoController mController;
    private FrameLayout mContainer;

    public NiceVideoPlayer(Context context) {
        this(context, null);
    }

    public NiceVideoPlayer(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    private void init() {
         mContainer = new FrameLayout(mContext);
         mContainer.setBackgroundColor(Color.BLACK);
         LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);
    }
}複製代碼

添加setUp方法來配置播放的視頻資源路徑(本地/網絡資源):

public void setUp(String url, Map
  
  
  

 
  
  headers) { mUrl = url; mHeaders = headers; } 

 複製代碼

用戶要在mController中操做才能播放,所以須要在播放以前設置好mController:

public void setController(NiceVideoPlayerController controller) {
    mController = controller;
    mController.setNiceVideoPlayer(this);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mController, params);
}複製代碼

用戶在自定義好本身的控制器後經過setController這個方法設置給播放器進行關聯。

觸發播放時,NiceVideoPlayer將展現視頻圖像內容的mTextureView添加到mContainer中(在mController的下層),同時初始化mMediaPlayer,待mTextureView的數據通道SurfaceTexture準備就緒後就能夠打開播放器:

public void start() {
    initMediaPlayer();  // 初始化播放器
    initTextureView();  // 初始化展現視頻內容的TextureView
    addTextureView();   // 將TextureView添加到容器中
}

private void initTextureView() {
    if (mTextureView == null) {
        mTextureView = new TextureView(mContext);
        mTextureView.setSurfaceTextureListener(this);
    }
}

private void addTextureView() {
    mContainer.removeView(mTextureView);
    LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mTextureView, 0,  params);
}

private void initMediaPlayer() {
    if (mMediaPlayer == null) {
        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);

        mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
        mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
        mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
        mMediaPlayer.setOnErrorListener(mOnErrorListener);
        mMediaPlayer.setOnInfoListener(mOnInfoListener);
        mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
    }
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    // surfaceTexture數據通道準備就緒,打開播放器
    openMediaPlayer(surface);
}

private void openMediaPlayer(SurfaceTexture surface) {
    try {
        mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUrl), mHeaders);
        mMediaPlayer.setSurface(new Surface(surface));
        mMediaPlayer.prepareAsync();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}複製代碼

打開播放器調用prepareAsync()方法後,mMediaPlayer進入準備狀態,準備就緒後就能夠開始:

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
    }
};複製代碼

NiceVideoPlayer的這些邏輯已經實現視頻播放了,操做相關以及UI展現的邏輯須要在控制器NiceVideoPlayerController中來實現。可是呢,UI的展現和反饋都須要依據播放器當前的播放狀態,因此須要給播放器定義一些常量來表示它的播放狀態:

public static final int STATE_ERROR = -1;          // 播放錯誤
public static final int STATE_IDLE = 0;            // 播放未開始
public static final int STATE_PREPARING = 1;       // 播放準備中
public static final int STATE_PREPARED = 2;        // 播放準備就緒
public static final int STATE_PLAYING = 3;         // 正在播放
public static final int STATE_PAUSED = 4;          // 暫停播放
// 正在緩衝(播放器正在播放時,緩衝區數據不足,進行緩衝,緩衝區數據足夠後恢復播放)
public static final int STATE_BUFFERING_PLAYING = 5;
// 正在緩衝(播放器正在播放時,緩衝區數據不足,進行緩衝,此時暫停播放器,繼續緩衝,緩衝區數據足夠後恢復暫停)
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7;       // 播放完成複製代碼

播放視頻時,mMediaPlayer準備就緒(Prepared)後沒有立刻進入播放狀態,中間有一個時間延遲時間段,而後開始渲染圖像。因此將Prepared——>「開始渲染」中間這個時間段定義爲STATE_PREPARED

若是是播放網絡視頻,在播放過程當中,緩衝區數據不足時mMediaPlayer內部會停留在某一幀畫面以進行緩衝。正在緩衝時,mMediaPlayer多是在正在播放也多是暫停狀態,由於在緩衝時若是用戶主動點擊了暫停,就是處於STATE_BUFFERING_PAUSED,因此緩衝有STATE_BUFFERING_PLAYINGSTATE_BUFFERING_PAUSED兩種狀態,緩衝結束後,恢復播放或暫停。

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
        mCurrentState = STATE_PREPARED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onPrepared ——> STATE_PREPARED");
    }
};

private MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener
        = new MediaPlayer.OnVideoSizeChangedListener() {
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        LogUtil.d("onVideoSizeChanged ——> width:" + width + ",height:" + height);
    }
};

private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
    }
};

private MediaPlayer.OnErrorListener mOnErrorListener
        = new MediaPlayer.OnErrorListener() {
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mCurrentState = STATE_ERROR;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onError ——> STATE_ERROR ———— what:" + what);
        return false;
    }
};

private MediaPlayer.OnInfoListener mOnInfoListener
        = new MediaPlayer.OnInfoListener() {
    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
            // 播放器渲染第一幀
            mCurrentState = STATE_PLAYING;
            mController.setControllerState(mPlayerState, mCurrentState);
            LogUtil.d("onInfo ——> MEDIA_INFO_VIDEO_RENDERING_START:STATE_PLAYING");
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
            // MediaPlayer暫時不播放,以緩衝更多的數據
            if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_BUFFERING_PAUSED;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PAUSED");
            } else {
                mCurrentState = STATE_BUFFERING_PLAYING;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PLAYING");
            }
            mController.setControllerState(mPlayerState, mCurrentState);
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
            // 填充緩衝區後,MediaPlayer恢復播放/暫停
            if (mCurrentState == STATE_BUFFERING_PLAYING) {
                mCurrentState = STATE_PLAYING;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PLAYING");
            }
            if (mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_PAUSED;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PAUSED");
            }
        } else {
            LogUtil.d("onInfo ——> what:" + what);
        }
        return true;
    }
};

private MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener
        = new MediaPlayer.OnBufferingUpdateListener() {
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        mBufferPercentage = percent;
    }
};複製代碼

mController.setControllerState(mPlayerState, mCurrentState)mCurrentState表示當前播放狀態,mPlayerState表示播放器的全屏、小窗口,正常三種狀態。

public static final int PLAYER_NORMAL = 10;        // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11;   // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12;   // 小窗口播放器複製代碼

定義好播放狀態後,開始暫停等操做邏輯也須要根據播放狀態調整:

@Override
public void start() {
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

@Override
public void restart() {
    if (mCurrentState == STATE_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PLAYING");
    }
    if (mCurrentState == STATE_BUFFERING_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_BUFFERING_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PLAYING");
    }
}

@Override
public void pause() {
    if (mCurrentState == STATE_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PAUSED");
    }
    if (mCurrentState == STATE_BUFFERING_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_BUFFERING_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PAUSED");
    }
}複製代碼

reStart()方法是暫停時繼續播放調用。

全屏、小窗口播放的實現

可能最能想到實現全屏的方式就是把當前播放器的寬高給放大到屏幕大小,同時隱藏除播放器之外的其餘全部UI,並設置成橫屏模式。可是這種方式有不少問題,好比在列表(ListView或RecyclerView)中,除了放大隱藏外,還須要去計算滑動多少距離才恰好讓播放器與屏幕邊緣重合,退出全屏的時候還須要滑動到以前的位置,這樣實現邏輯不但繁瑣,並且和外部UI偶合嚴重,後面改動維護起來很是困難(我曾經就用這種方式被坑了無數道)。

分析能不能有其餘更好的實現方式呢?

整個播放器由mMediaPalyer+mTexutureView+mController組成,要實現全屏或小窗口播放,咱們只須要挪動播放器的展現界面mTexutureView和控制界面mController便可。而且呢咱們在上面定義播放器時,已經把mTexutureViewmController一塊兒添加到mContainer中了,因此只須要將mContainer從當前視圖中移除,並添加到全屏和小窗口的目標視圖中便可。

那麼怎麼肯定全屏和小窗口的目標視圖呢?

咱們知道每一個Activity裏面都有一個android.R.content,它是一個FrameLayout,裏面包含了咱們setContentView的全部控件。既然它是一個FrameLayout,咱們就能夠將它做爲全屏和小窗口的目標視圖。

咱們把從當前視圖移除的mContainer從新添加到android.R.content中,而且設置成橫屏。這個時候還須要注意android.R.content是不包括ActionBar和狀態欄的,因此要將Activity設置成全屏模式,同時隱藏ActionBar

@Override
public void enterFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) return;

    // 隱藏ActionBar、狀態欄,並橫屏
    NiceUtil.hideActionBar(mContext);
    NiceUtil.scanForActivity(mContext)
            .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

    this.removeView(mContainer);
    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_FULL_SCREEN;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_FULL_SCREEN");
}複製代碼

退出全屏也就很簡單了,將mContainerandroid.R.content中移除,從新添加到當前視圖,並恢復ActionBar、清除全屏模式就好了。

@Override
public boolean exitFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) {
        NiceUtil.showActionBar(mContext);
        NiceUtil.scanForActivity(mContext)
                .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}複製代碼

切換橫豎屏時爲了不Activity從新走生命週期,別忘了須要在Manifest.xmlactivity標籤下添加以下配置:

android:configChanges="orientation|keyboardHidden|screenSize"複製代碼

進入小窗口播放和退出小窗口的實現原理就和全屏功能同樣了,只須要修改它的寬高參數:

@Override
public void enterTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) return;

    this.removeView(mContainer);

    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    // 小窗口的寬度爲屏幕寬度的60%,長寬比默認爲16:9,右邊距、下邊距爲8dp。
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f),
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f));
    params.gravity = Gravity.BOTTOM | Gravity.END;
    params.rightMargin = NiceUtil.dp2px(mContext, 8f);
    params.bottomMargin = NiceUtil.dp2px(mContext, 8f);

    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_TINY_WINDOW;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_TINY_WINDOW");
}

@Override
public boolean exitTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) {
        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}複製代碼

這裏有個特別須要注意的一點:

mContainer移除從新添加後,mContainer及其內部的mTextureViewmController都會重繪,mTextureView重繪後,會從新new一個SurfaceTexture,並從新回調onSurfaceTextureAvailable方法,這樣mTextureView的數據通道SurfaceTexture發生了變化,可是mMediaPlayer仍是持有原先的mSurfaceTexut,因此在切換全屏以前要保存以前的mSufaceTexture,當切換到全屏後從新調用onSurfaceTextureAvailable時,將以前的mSufaceTexture從新設置給mTexutureView。這樣講保證了切換時視頻播放的無縫銜接。

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    if (mSurfaceTexture == null) {
        mSurfaceTexture = surfaceTexture;
        openMediaPlayer();
    } else {
        mTextureView.setSurfaceTexture(mSurfaceTexture);
    }
}複製代碼

NiceVideoPlayerControl

爲了解除NiceVideoPlayerNiceVideoPlayerController的耦合,把NiceVideoPlayer的一些功能性和判斷性方法抽象到NiceVideoPlayerControl接口中。

public interface NiceVideoPlayerControl {

    void start();
    void restart();
    void pause();
    void seekTo(int pos);

    boolean isIdle();
    boolean isPreparing();
    boolean isPrepared();
    boolean isBufferingPlaying();
    boolean isBufferingPaused();
    boolean isPlaying();
    boolean isPaused();
    boolean isError();
    boolean isCompleted();

    boolean isFullScreen();
    boolean isTinyWindow();
    boolean isNormal();

    int getDuration();
    int getCurrentPosition();
    int getBufferPercentage();

    void enterFullScreen();
    boolean exitFullScreen();
    void enterTinyWindow();
    boolean exitTinyWindow();

    void release();
}複製代碼

NiceVideoPlayer實現這個接口便可。

NiceVideoPlayerManager

同一界面上有多個視頻,或者視頻放在ReclerView或者ListView的容器中,要保證同一時刻只有一個視頻在播放,其餘的都是初始狀態,因此須要一個NiceVideoPlayerManager來管理播放器,主要功能是保存當前已經開始了的播放器。

public class NiceVideoPlayerManager {

    private NiceVideoPlayer mVideoPlayer;

    private NiceVideoPlayerManager() {
    }

    private static NiceVideoPlayerManager sInstance;

    public static synchronized NiceVideoPlayerManager instance() {
        if (sInstance == null) {
            sInstance = new NiceVideoPlayerManager();
        }
        return sInstance;
    }

    public void setCurrentNiceVideoPlayer(NiceVideoPlayer videoPlayer) {
        mVideoPlayer = videoPlayer;
    }

    public void releaseNiceVideoPlayer() {
        if (mVideoPlayer != null) {
            mVideoPlayer.release();
            mVideoPlayer = null;
        }
    }

    public boolean onBackPressd() {
        if (mVideoPlayer != null) {
            if (mVideoPlayer.isFullScreen()) {
                return mVideoPlayer.exitFullScreen();
            } else if (mVideoPlayer.isTinyWindow()) {
                return mVideoPlayer.exitTinyWindow();
            } else {
                mVideoPlayer.release();
                return false;
            }
        }
        return false;
    }
}複製代碼

採用單例,同時,onBackPressedActivity中用戶按返回鍵時調用。
NiceVideoPlayerstart方法以及onCompleted須要修改一下,保證開始播放一個視頻時要先釋放掉以前的播放器;同時本身播放完畢,要將NiceVideoPlayerManager中的mNiceVideoPlayer實例置空,避免內存泄露。

// NiceVideoPlayer的start()方法。
@Override
public void start() {
    NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
    NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(this);
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

// NiceVideoPlayer中的onCompleted監聽。
private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
        NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(null);
    }
};複製代碼

NiceVideoPlayerController

播放控制界面上,播放、暫停、播放進度、緩衝動畫、全屏/小屏等觸發都是直接調用播放器對應的操做的。須要注意的就是調用以前要判斷當前的播放狀態,由於有些狀態下調用播放器的操做可能引發錯誤(好比播放器還沒準備就緒,就去獲取當前的播放位置)。

播放器在觸發相應功能的時候都會調用NiceVideoPlayerControllersetControllerState(int playerState, int playState)這個方法來讓用戶修改UI。

不一樣項目均可能定製不一樣的控制器(播放操做界面),這裏我就不詳細分析實現邏輯了,大體功能就相似騰訊視頻的熱點列表中的播放器。其中全屏模式下橫向滑動改變播放進度、左側上下滑動改變亮度,右側上下滑動改變亮度等功能代碼中並未實現,有須要的能夠直接參考節操播放器,只須要在ControlleronInterceptTouchEvent中處理就好了(後續會添加上去)。

代碼有點長,就不貼了,須要的直接下載源碼

使用

mNiceVideoPlayer.setUp(url, null);
NiceVideoPlayerController controller = new NiceVideoPlayerController(this);
controller.setTitle(title);
controller.setImage(imageUrl);
mNiceVideoPlayer.setController(controller);複製代碼

RecyclerView或者ListView中使用時,須要監聽itemViewdetached

mRecyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
    @Override
    public void onChildViewAttachedToWindow(View view) {

    }

    @Override
    public void onChildViewDetachedFromWindow(View view) {
        NiceVideoPlayer niceVideoPlayer = (NiceVideoPlayer) view.findViewById(R.id.nice_video_player);
        if (niceVideoPlayer != null) {
            niceVideoPlayer.release();
        }
    }
});複製代碼

ItemViewdetach窗口時,須要釋放掉itemView內部的播放器。

效果圖

最後

整個功能有參考節操播放器,可是本身這樣封裝和節操播放器仍是有很大差別:一是分離了播放功能和控制界面,定製只需修改控制器便可。二是全屏/小窗口沒有新建一個播放器,只是挪動了播放界面和控制器,不用每一個視頻都須要新建兩個播放器,也不用同步狀態。


MediaPlayer有不少格式不支持,項目已添加IjkPlayer的擴展支持,能夠切換IjkPlayer和原生MediaPlayer,後續還會考慮添加ExoPlayer,同時也會擴展更多功能。

若是有錯誤和更好的建議都請提出,源碼已上傳GitHub,歡迎Star,謝謝!。

源碼:github.com/xiaoyanger0…


參考:
Android TextureView簡易教程
視頻畫面幀的展現控件SurfaceView及TextureView對比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命週期詳解
節操播放器 https://github.com/lipangit/JieCaoVideoPlayer

相關文章
相關標籤/搜索