項目已添加IjkPlayer支持,後續逐漸完善其餘功能。
地址:github.com/xiaoyanger0…html
在Android總播放視頻能夠直接使用VideoView
,VideoView
是經過繼承自SurfaceView
來實現的。SurfaceView
的大概原理就是在現有View
的位置上建立一個新的Window
,內容的顯示和渲染都在新的Window
中。這使得SurfaceView
的繪製和刷新能夠在單獨的線程中進行,從而大大提升效率。可是呢,因爲SurfaceView
的內容沒有顯示在View
中而是顯示在新建的Window
中, 使得SurfaceView
的顯示不受View
的屬性控制,不能進行平移,縮放等變換,也不能放在其它RecyclerView
或ScrollView
中,一些View
中的特性也沒法使用。android
TextureView
是在4.0(API level 14)引入的,與SurfaceView
相比,它不會建立新的窗口來顯示內容。它是將內容流直接投放到View
中,而且能夠和其它普通View
同樣進行移動,旋轉,縮放,動畫等變化。TextureView
必須在硬件加速的窗口中使用。git
TextureView
被建立後不能直接使用,必需要在它被它添加到ViewGroup
後,待SurfaceTexture
準備就緒才能起做用(看TextureView
的源碼,TextureView
是在繪製的時候建立的內部SurfaceTexture
)。一般須要給TextureView
設置監聽器SurfaceTextuListener
:github
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
是Android原生的多媒體播放器,能夠用它來實現本地或者在線音視頻的播放,同時它支持https和rtsp。ide
MediaPlayer
定義了各類狀態,能夠理解爲是它的生命週期。oop
這個狀態圖描述了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
就能夠了。
首先,須要一個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, Mapheaders) { 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
、清除全屏模式就好了。
@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.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);
}
}複製代碼
爲了解除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
實現這個接口便可。
同一界面上有多個視頻,或者視頻放在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
的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();
}
}
});複製代碼
在ItemView
detach窗口時,須要釋放掉itemView
內部的播放器。
整個功能有參考節操播放器,可是本身這樣封裝和節操播放器仍是有很大差別:一是分離了播放功能和控制界面,定製只需修改控制器便可。二是全屏/小窗口沒有新建一個播放器,只是挪動了播放界面和控制器,不用每一個視頻都須要新建兩個播放器,也不用同步狀態。
MediaPlayer
有不少格式不支持,項目已添加IjkPlayer
的擴展支持,能夠切換IjkPlayer
和原生MediaPlayer
,後續還會考慮添加ExoPlayer
,同時也會擴展更多功能。
若是有錯誤和更好的建議都請提出,源碼已上傳GitHub,歡迎Star,謝謝!。
參考:
Android TextureView簡易教程
視頻畫面幀的展現控件SurfaceView及TextureView對比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命週期詳解
節操播放器 https://github.com/lipangit/JieCaoVideoPlayer