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

ksgc0018 7年前發布 | 33K 次閱讀 安卓開發 Android開發 移動開發

為什么使用TextureView

在Android總播放視頻可以直接使用 VideoView , VideoView 是通過繼承自 SurfaceView 來實現的。 SurfaceView 的大概原理就是在現有 View 的位置上創建一個新的 Window ,內容的顯示和渲染都在新的 Window 中。這使得 SurfaceView 的繪制和刷新可以在單獨的線程中進行,從而大大提高效率。但是呢,由于 SurfaceView 的內容沒有顯示在 View 中而是顯示在新建的 Window 中, 使得 SurfaceView 的顯示不受 View 的屬性控制,不能進行平移,縮放等變換,也不能放在其它 RecyclerView 或 ScrollView 中,一些 View 中的特性也無法使用。

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

TextureView 被創建后不能直接使用,必須要在它被它添加到 ViewGroup 后,待 SurfaceTexture 準備就緒才能起作用(看 TextureView 的源碼, TextureView 是在繪制的時候創建的內部 SurfaceTexture )。通常需要給 TextureView 設置監聽器 SurfaceTextuListener :

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更新
}

});</code></pre>

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

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

MediaPlayer介紹

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

MediaPlayer 定義了各種狀態,可以理解為是它的生命周期。

MediaPlayer狀態圖(生命周期)

這個狀態圖描述了 MediaPlayer 的各種狀態,以及主要方法調用后的狀態變化。

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

方法 介紹 狀態
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);
}

}</code></pre>

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

public void setUp(String url, Map<String, String> 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) {

}</code></pre>

打開播放器調用 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_PLAYING 和 STATE_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; } };</code></pre>

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"); } }</code></pre>

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

全屏、小窗口播放的實現

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

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

整個播放器由 mMediaPalyer + mTexutureView + mController 組成,要實現全屏或小窗口播放,我們只需要挪動播放器的展示界面 mTexutureView 和控制界面 mController 即可。并且呢我們在上面定義播放器時,已經把 mTexutureView 和 mController 一起添加到 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);
contentView.addView(mContainer);

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

}</code></pre>

退出全屏也就很簡單了,將 mContainer 從 android.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);
    this.addView(mContainer);

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

}</code></pre>

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

@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); this.addView(mContainer);

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

}</code></pre>

這里有個特別需要注意的一點:

當 mContainer 移除重新添加后, mContainer 及其內部的 mTextureView 和 mController 都會重繪, 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

為了解除 NiceVideoPlayer 和 NiceVideoPlayerController 的耦合,把 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();

}</code></pre>

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;
}

}</code></pre>

采用單例,同時, onBackPressed 供 Activity 中用戶按返回鍵時調用。

NiceVideoPlayer 的 start 方法以及 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); } };</code></pre>

NiceVideoPlayerController

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

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

不同項目都可能定制不同的控制器(播放操作界面),這里我就不詳細分析實現邏輯了,大致功能就類似騰訊視頻的熱點列表中的播放器。其中全屏模式下橫向滑動改變播放進度、左側上下滑動改變亮度,右側上下滑動改變亮度等功能代碼中并未實現,有需要的可以直接參考 節操播放器 ,只需要在 Controller 的 onInterceptTouchEvent 中處理就行了(后續會添加上去)。

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

使用

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

在 RecyclerView 或者 ListView 中使用時,需要監聽 itemView 的 detached :

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();
    }
}

});</code></pre>

在 ItemView detach窗口時,需要釋放掉 itemView 內部的播放器。

效果圖

最后

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

MediaPlayer有很多格式不支持,后面會考慮用IjkPlayer或者ExoPlayer封裝。

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

 本文由用戶 ksgc0018 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!