項目已添加IjkPlayer支持,后續逐漸完善其他功能。
地址:https://github.com/xiaoyanger0825/NiceVieoPlayer
為什么使用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更新
}
});
SurfaceTexture
的準備就緒、大小變化、銷毀、更新等狀態變化時都會回調相對應的方法。當TextureView
內部創建好SurfaceTexture
后,在監聽器的onSurfaceTextureAvailable
方法中,用SurfaceTexture
來關聯MediaPlayer
,作為播放視頻的圖像數據來源。
SurfaceTexture
作為數據通道,把從數據源(MediaPlayer
)中獲取到的圖像幀數據轉為GL外部紋理,交給TextureVeiw
作為View heirachy
中的一個硬件加速層來顯示,從而實現視頻播放功能。
MediaPlayer介紹
MediaPlayer
是Android原生的多媒體播放器,可以用它來實現本地或者在線音視頻的播放,同時它支持https和rtsp。
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);
}
}
添加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) {
}
打開播放器調用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;
}
};
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
即可。并且呢我們在上面定義播放器時,已經把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);
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");
}
退出全屏也就很簡單了,將mContainer
從android.R.content
中移除,重新添加到當前視圖,并恢復ActionBar
、清除全屏模式就行了。
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.xml
的activity
標簽下添加如下配置:
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
及其內部的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();
}
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;
}
}
采用單例,同時,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);
}
};
NiceVideoPlayerController
播放控制界面上,播放、暫停、播放進度、緩沖動畫、全屏/小屏等觸發都是直接調用播放器對應的操作的。需要注意的就是調用之前要判斷當前的播放狀態,因為有些狀態下調用播放器的操作可能引起錯誤(比如播放器還沒準備就緒,就去獲取當前的播放位置)。
播放器在觸發相應功能的時候都會調用NiceVideoPlayerController
的setControllerState(int playerState, int playState)
這個方法來讓用戶修改UI。
不同項目都可能定制不同的控制器(播放操作界面),這里我就不詳細分析實現邏輯了,大致功能就類似騰訊視頻的熱點列表中的播放器。其中橫向滑動改變播放進度、左側上下滑動改變亮度,右側上下滑動改變亮度等功能在代碼中都有實現。代碼有點長,就不貼了,需要的直接下載源碼。
使用
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();
}
}
});
在ItemView
detach窗口時,需要釋放掉itemView
內部的播放器。
效果圖
最后
整個功能有參考節操播放器,但是自己這樣封裝和節操播放器還是有很大差異:一是分離了播放功能和控制界面,定制只需修改控制器即可。二是全屏/小窗口沒有新建一個播放器,只是挪動了播放界面和控制器,不用每個視頻都需要新建兩個播放器,也不用同步狀態。
MediaPlayer
有很多格式不支持,項目已添加IjkPlayer
的擴展支持,可以切換IjkPlayer
和原生MediaPlayer
,后續還會考慮添加ExoPlayer
,同時也會擴展更多功能。
如果有錯誤和更好的建議都請提出,源碼已上傳GitHub,歡迎Star,謝謝!。
源碼:https://github.com/xiaoyanger0825/NiceVieoPlayer
參考:
Android TextureView簡易教程
視頻畫面幀的展示控件SurfaceView及TextureView對比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期詳解
節操播放器 https://github.com/lipangit/JieCaoVideoPlayer