專案已新增IjkPlayer支援,後續逐漸完善其他功能。
地址:github.com/xiaoyanger0…
為什麼使用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 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
、清除全屏模式就行了。
@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);
}
}複製程式碼
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。
不同專案都可能定製不同的控制器(播放操作介面),這裡我就不詳細分析實現邏輯了,大致功能就類似騰訊視訊的熱點列表中的播放器。其中全屏模式下橫向滑動改變播放進度、左側上下滑動改變亮度,右側上下滑動改變亮度等功能程式碼中並未實現,有需要的可以直接參考節操播放器,只需要在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