項(xiàng)目已添加IjkPlayer支持,后續(xù)逐漸完善其他功能。
地址:https://github.com/xiaoyanger0825/NiceVieoPlayer
為什么使用TextureView
在Android總播放視頻可以直接使用VideoView
,VideoView
是通過繼承自SurfaceView
來實(shí)現(xiàn)的。SurfaceView
的大概原理就是在現(xiàn)有View
的位置上創(chuàng)建一個(gè)新的Window
,內(nèi)容的顯示和渲染都在新的Window
中。這使得SurfaceView
的繪制和刷新可以在單獨(dú)的線程中進(jìn)行,從而大大提高效率。但是呢,由于SurfaceView
的內(nèi)容沒有顯示在View
中而是顯示在新建的Window
中, 使得SurfaceView
的顯示不受View
的屬性控制,不能進(jìn)行平移,縮放等變換,也不能放在其它RecyclerView
或ScrollView
中,一些View
中的特性也無法使用。
TextureView
是在4.0(API level 14)引入的,與SurfaceView
相比,它不會創(chuàng)建新的窗口來顯示內(nèi)容。它是將內(nèi)容流直接投放到View
中,并且可以和其它普通View
一樣進(jìn)行移動,旋轉(zhuǎn),縮放,動畫等變化。TextureView
必須在硬件加速的窗口中使用。
TextureView
被創(chuàng)建后不能直接使用,必須要在它被它添加到ViewGroup
后,待SurfaceTexture
準(zhǔn)備就緒才能起作用(看TextureView
的源碼,TextureView
是在繪制的時(shí)候創(chuàng)建的內(nèi)部SurfaceTexture
)。通常需要給TextureView
設(shè)置監(jiān)聽器SurfaceTextuListener
:
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// SurfaceTexture準(zhǔn)備就緒
}
@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
的準(zhǔn)備就緒、大小變化、銷毀、更新等狀態(tài)變化時(shí)都會回調(diào)相對應(yīng)的方法。當(dāng)TextureView
內(nèi)部創(chuàng)建好SurfaceTexture
后,在監(jiān)聽器的onSurfaceTextureAvailable
方法中,用SurfaceTexture
來關(guān)聯(lián)MediaPlayer
,作為播放視頻的圖像數(shù)據(jù)來源。
SurfaceTexture
作為數(shù)據(jù)通道,把從數(shù)據(jù)源(MediaPlayer
)中獲取到的圖像幀數(shù)據(jù)轉(zhuǎn)為GL外部紋理,交給TextureVeiw
作為View heirachy
中的一個(gè)硬件加速層來顯示,從而實(shí)現(xiàn)視頻播放功能。
MediaPlayer介紹
MediaPlayer
是Android原生的多媒體播放器,可以用它來實(shí)現(xiàn)本地或者在線音視頻的播放,同時(shí)它支持https和rtsp。
MediaPlayer
定義了各種狀態(tài),可以理解為是它的生命周期。
這個(gè)狀態(tài)圖描述了MediaPlayer
的各種狀態(tài),以及主要方法調(diào)用后的狀態(tài)變化。
MediaPlayer的相關(guān)方法及監(jiān)聽接口:
方法 | 介紹 | 狀態(tài) |
---|---|---|
setDataSource | 設(shè)置數(shù)據(jù)源 | Initialized |
prepare | 準(zhǔn)備播放,同步 | Preparing —> Prepared |
prepareAsync | 準(zhǔn)備播放,異步 | Preparing —> Prepared |
start | 開始或恢復(fù)播放 | Started |
pause | 暫停 | Paused |
stop | 停止 | Stopped |
seekTo | 到指定時(shí)間點(diǎn)位置 | PrePared/Started |
reset | 重置播放器 | Idle |
setAudioStreamType | 設(shè)置音頻流類型 | -- |
setDisplay | 設(shè)置播放視頻的Surface | -- |
setVolume | 設(shè)置聲音 | -- |
getBufferPercentage | 獲取緩沖半分比 | -- |
getCurrentPosition | 獲取當(dāng)前播放位置 | -- |
getDuration | 獲取播放文件總時(shí)間 | -- |
內(nèi)部回調(diào)接口 | 介紹 | 狀態(tài) |
---|---|---|
OnPreparedListener | 準(zhǔn)備監(jiān)聽 | Preparing ——>Prepared |
OnVideoSizeChangedListener | 視頻尺寸變化監(jiān)聽 | -- |
OnInfoListener | 指示信息和警告信息監(jiān)聽 | -- |
OnCompletionListener | 播放完成監(jiān)聽 | PlaybackCompleted |
OnErrorListener | 播放錯(cuò)誤監(jiān)聽 | Error |
OnBufferingUpdateListener | 緩沖更新監(jiān)聽 | -- |
MediaPlayer
在直接new出來之后就進(jìn)入了Idle狀態(tài),此時(shí)可以調(diào)用多個(gè)重載的setDataSource()
方法從idle狀態(tài)進(jìn)入Initialized狀態(tài)(如果調(diào)用setDataSource()
方法的時(shí)候,MediaPlayer
對象不是出于Idle狀態(tài),會拋異常,可以調(diào)用reset()
方法回到Idle狀態(tài))。
調(diào)用prepared()
方法和preparedAsync()
方法進(jìn)入Prepared狀態(tài),prepared()方法直接進(jìn)入Parpared狀態(tài),preparedAsync()方法會先進(jìn)入PreParing狀態(tài),播放引擎準(zhǔn)備完畢后會通過OnPreparedListener.onPrepared()
回調(diào)方法通知Prepared狀態(tài)。
在Prepared狀態(tài)下就可以調(diào)用start()方法進(jìn)行播放了,此時(shí)進(jìn)入started()狀態(tài),如果播放的是網(wǎng)絡(luò)資源,Started狀態(tài)下也會自動調(diào)用客戶端注冊的OnBufferingUpdateListener.OnBufferingUpdate()
回調(diào)方法,對流播放緩沖的狀態(tài)進(jìn)行追蹤。
pause()
方法和start()
方法是對應(yīng)的,調(diào)用pause()
方法會進(jìn)入Paused狀態(tài),調(diào)用start()
方法重新進(jìn)入Started狀態(tài),繼續(xù)播放。
stop()
方法會使MdiaPlayer
從Started、Paused、Prepared、PlaybackCompleted等狀態(tài)進(jìn)入到Stoped狀態(tài),播放停止。
當(dāng)資源播放完畢時(shí),如果調(diào)用了setLooping(boolean)
方法,會自動進(jìn)入Started狀態(tài)重新播放,如果沒有調(diào)用則會自動調(diào)用客戶端播放器注冊的OnCompletionListener.OnCompletion()
方法,此時(shí)MediaPlayer
進(jìn)入PlaybackCompleted狀態(tài),在此狀態(tài)里可以調(diào)用start()
方法重新進(jìn)入Started狀態(tài)。
封裝考慮
MediaPlayer
的方法和接口比較多,不同的狀態(tài)調(diào)用各個(gè)方法后狀態(tài)變化情況也比較復(fù)雜。播放相關(guān)的邏輯只與MediaPlayer
的播放狀態(tài)和調(diào)用方法相關(guān),而界面展示和UI操作很多時(shí)候都需要根據(jù)自己項(xiàng)目來定制。參考原生的VideoView
,為了解耦和方便定制,把MediaPlayer
的播放邏輯和UI界面展示及操作相關(guān)的邏輯分離。我是把MediaPlayer
直接封裝到NiceVideoPlayer
中,各種UI狀態(tài)和操作反饋都封裝到NiceVideoPlayerController
里面。如果需要根據(jù)不同的項(xiàng)目需求來修改播放器的功能,就只重寫NiceVideoPlayerController
就可以了。
NiceVideoPlayer
首先,需要一個(gè)FrameLayout
容器mContainer
,里面有兩層內(nèi)容,第一層就是展示播放視頻內(nèi)容的TextureView
,第二層就是播放器控制器mController
。那么自定義一個(gè)NiceVideoPlayer
繼承自FrameLayout
,將mContainer
添加到當(dāng)前控件:
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
方法來配置播放的視頻資源路徑(本地/網(wǎng)絡(luò)資源):
public void setUp(String url, Map<String, String> headers) {
mUrl = url;
mHeaders = headers;
}
用戶要在mController
中操作才能播放,因此需要在播放之前設(shè)置好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
這個(gè)方法設(shè)置給播放器進(jìn)行關(guān)聯(lián)。
觸發(fā)播放時(shí),NiceVideoPlayer
將展示視頻圖像內(nèi)容的mTextureView
添加到mContainer
中(在mController
的下層),同時(shí)初始化mMediaPlayer
,待mTextureView
的數(shù)據(jù)通道SurfaceTexture
準(zhǔn)備就緒后就可以打開播放器:
public void start() {
initMediaPlayer(); // 初始化播放器
initTextureView(); // 初始化展示視頻內(nèi)容的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數(shù)據(jù)通道準(zhǔn)備就緒,打開播放器
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) {
}
打開播放器調(diào)用prepareAsync()
方法后,mMediaPlayer
進(jìn)入準(zhǔn)備狀態(tài),準(zhǔn)備就緒后就可以開始:
private MediaPlayer.OnPreparedListener mOnPreparedListener
= new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
};
NiceVideoPlayer
的這些邏輯已經(jīng)實(shí)現(xiàn)視頻播放了,操作相關(guān)以及UI展示的邏輯需要在控制器NiceVideoPlayerController
中來實(shí)現(xiàn)。但是呢,UI的展示和反饋都需要依據(jù)播放器當(dāng)前的播放狀態(tài),所以需要給播放器定義一些常量來表示它的播放狀態(tài):
public static final int STATE_ERROR = -1; // 播放錯(cuò)誤
public static final int STATE_IDLE = 0; // 播放未開始
public static final int STATE_PREPARING = 1; // 播放準(zhǔn)備中
public static final int STATE_PREPARED = 2; // 播放準(zhǔn)備就緒
public static final int STATE_PLAYING = 3; // 正在播放
public static final int STATE_PAUSED = 4; // 暫停播放
// 正在緩沖(播放器正在播放時(shí),緩沖區(qū)數(shù)據(jù)不足,進(jìn)行緩沖,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)播放)
public static final int STATE_BUFFERING_PLAYING = 5;
// 正在緩沖(播放器正在播放時(shí),緩沖區(qū)數(shù)據(jù)不足,進(jìn)行緩沖,此時(shí)暫停播放器,繼續(xù)緩沖,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)暫停)
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7; // 播放完成
播放視頻時(shí),mMediaPlayer
準(zhǔn)備就緒(Prepared
)后沒有馬上進(jìn)入播放狀態(tài),中間有一個(gè)時(shí)間延遲時(shí)間段,然后開始渲染圖像。所以將Prepared——>“開始渲染”中間這個(gè)時(shí)間段定義為STATE_PREPARED
。
如果是播放網(wǎng)絡(luò)視頻,在播放過程中,緩沖區(qū)數(shù)據(jù)不足時(shí)mMediaPlayer
內(nèi)部會停留在某一幀畫面以進(jìn)行緩沖。正在緩沖時(shí),mMediaPlayer
可能是在正在播放也可能是暫停狀態(tài),因?yàn)樵诰彌_時(shí)如果用戶主動點(diǎn)擊了暫停,就是處于STATE_BUFFERING_PAUSED
,所以緩沖有STATE_BUFFERING_PLAYING
和STATE_BUFFERING_PAUSED
兩種狀態(tài),緩沖結(jié)束后,恢復(fù)播放或暫停。
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暫時(shí)不播放,以緩沖更多的數(shù)據(jù)
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) {
// 填充緩沖區(qū)后,MediaPlayer恢復(fù)播放/暫停
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
表示當(dāng)前播放狀態(tài),mPlayerState
表示播放器的全屏、小窗口,正常三種狀態(tài)。
public static final int PLAYER_NORMAL = 10; // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11; // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12; // 小窗口播放器
定義好播放狀態(tài)后,開始暫停等操作邏輯也需要根據(jù)播放狀態(tài)調(diào)整:
@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()
方法是暫停時(shí)繼續(xù)播放調(diào)用。
全屏、小窗口播放的實(shí)現(xiàn)
可能最能想到實(shí)現(xiàn)全屏的方式就是把當(dāng)前播放器的寬高給放大到屏幕大小,同時(shí)隱藏除播放器以外的其他所有UI,并設(shè)置成橫屏模式。但是這種方式有很多問題,比如在列表(ListView或RecyclerView
)中,除了放大隱藏外,還需要去計(jì)算滑動多少距離才剛好讓播放器與屏幕邊緣重合,退出全屏的時(shí)候還需要滑動到之前的位置,這樣實(shí)現(xiàn)邏輯不但繁瑣,而且和外部UI偶合嚴(yán)重,后面改動維護(hù)起來非常困難(我曾經(jīng)就用這種方式被坑了無數(shù)道)。
分析能不能有其他更好的實(shí)現(xiàn)方式呢?
整個(gè)播放器由mMediaPalyer
+mTexutureView
+mController
組成,要實(shí)現(xiàn)全屏或小窗口播放,我們只需要挪動播放器的展示界面mTexutureView
和控制界面mController
即可。并且呢我們在上面定義播放器時(shí),已經(jīng)把mTexutureView
和mController
一起添加到mContainer
中了,所以只需要將mContainer
從當(dāng)前視圖中移除,并添加到全屏和小窗口的目標(biāo)視圖中即可。
那么怎么確定全屏和小窗口的目標(biāo)視圖呢?
我們知道每個(gè)Activity
里面都有一個(gè)android.R.content
,它是一個(gè)FrameLayout
,里面包含了我們setContentView
的所有控件。既然它是一個(gè)FrameLayout
,我們就可以將它作為全屏和小窗口的目標(biāo)視圖。
我們把從當(dāng)前視圖移除的mContainer
重新添加到android.R.content
中,并且設(shè)置成橫屏。這個(gè)時(shí)候還需要注意android.R.content
是不包括ActionBar
和狀態(tài)欄的,所以要將Activity
設(shè)置成全屏模式,同時(shí)隱藏ActionBar
。
@Override
public void enterFullScreen() {
if (mPlayerState == PLAYER_FULL_SCREEN) return;
// 隱藏ActionBar、狀態(tài)欄,并橫屏
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
中移除,重新添加到當(dāng)前視圖,并恢復(fù)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;
}
切換橫豎屏?xí)r為了避免Activity
重新走生命周期,別忘了需要在Manifest.xml
的activity
標(biāo)簽下添加如下配置:
android:configChanges="orientation|keyboardHidden|screenSize"
進(jìn)入小窗口播放和退出小窗口的實(shí)現(xiàn)原理就和全屏功能一樣了,只需要修改它的寬高參數(shù):
@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%,長寬比默認(rèn)為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;
}
這里有個(gè)特別需要注意的一點(diǎn):
當(dāng)mContainer
移除重新添加后,mContainer
及其內(nèi)部的mTextureView
和mController
都會重繪,mTextureView
重繪后,會重新new
一個(gè)SurfaceTexture
,并重新回調(diào)onSurfaceTextureAvailable
方法,這樣mTextureView
的數(shù)據(jù)通道SurfaceTexture
發(fā)生了變化,但是mMediaPlayer
還是持有原先的mSurfaceTexut
,所以在切換全屏之前要保存之前的mSufaceTexture
,當(dāng)切換到全屏后重新調(diào)用onSurfaceTextureAvailable
時(shí),將之前的mSufaceTexture
重新設(shè)置給mTexutureView
。這樣就保證了切換時(shí)視頻播放的無縫銜接。
@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
實(shí)現(xiàn)這個(gè)接口即可。
NiceVideoPlayerManager
同一界面上有多個(gè)視頻,或者視頻放在ReclerView
或者ListView
的容器中,要保證同一時(shí)刻只有一個(gè)視頻在播放,其他的都是初始狀態(tài),所以需要一個(gè)NiceVideoPlayerManager
來管理播放器,主要功能是保存當(dāng)前已經(jīng)開始了的播放器。
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;
}
}
采用單例,同時(shí),onBackPressed
供Activity
中用戶按返回鍵時(shí)調(diào)用。
NiceVideoPlayer
的start
方法以及onCompleted
需要修改一下,保證開始播放一個(gè)視頻時(shí)要先釋放掉之前的播放器;同時(shí)自己播放完畢,要將NiceVideoPlayerManager
中的mNiceVideoPlayer
實(shí)例置空,避免內(nèi)存泄露。
// 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監(jiān)聽。
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
播放控制界面上,播放、暫停、播放進(jìn)度、緩沖動畫、全屏/小屏等觸發(fā)都是直接調(diào)用播放器對應(yīng)的操作的。需要注意的就是調(diào)用之前要判斷當(dāng)前的播放狀態(tài),因?yàn)橛行顟B(tài)下調(diào)用播放器的操作可能引起錯(cuò)誤(比如播放器還沒準(zhǔn)備就緒,就去獲取當(dāng)前的播放位置)。
播放器在觸發(fā)相應(yīng)功能的時(shí)候都會調(diào)用NiceVideoPlayerController
的setControllerState(int playerState, int playState)
這個(gè)方法來讓用戶修改UI。
不同項(xiàng)目都可能定制不同的控制器(播放操作界面),這里我就不詳細(xì)分析實(shí)現(xiàn)邏輯了,大致功能就類似騰訊視頻的熱點(diǎn)列表中的播放器。其中橫向滑動改變播放進(jìn)度、左側(cè)上下滑動改變亮度,右側(cè)上下滑動改變亮度等功能在代碼中都有實(shí)現(xiàn)。代碼有點(diǎn)長,就不貼了,需要的直接下載源碼。
使用
mNiceVideoPlayer.setUp(url, null);
NiceVideoPlayerController controller = new NiceVideoPlayerController(this);
controller.setTitle(title);
controller.setImage(imageUrl);
mNiceVideoPlayer.setController(controller);
在RecyclerView
或者ListView
中使用時(shí),需要監(jiān)聽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窗口時(shí),需要釋放掉itemView
內(nèi)部的播放器。
效果圖
最后
整個(gè)功能有參考節(jié)操播放器,但是自己這樣封裝和節(jié)操播放器還是有很大差異:一是分離了播放功能和控制界面,定制只需修改控制器即可。二是全屏/小窗口沒有新建一個(gè)播放器,只是挪動了播放界面和控制器,不用每個(gè)視頻都需要新建兩個(gè)播放器,也不用同步狀態(tài)。
MediaPlayer
有很多格式不支持,項(xiàng)目已添加IjkPlayer
的擴(kuò)展支持,可以切換IjkPlayer
和原生MediaPlayer
,后續(xù)還會考慮添加ExoPlayer
,同時(shí)也會擴(kuò)展更多功能。
如果有錯(cuò)誤和更好的建議都請?zhí)岢觯创a已上傳GitHub,歡迎Star,謝謝!。
源碼:https://github.com/xiaoyanger0825/NiceVieoPlayer
參考:
Android TextureView簡易教程
視頻畫面幀的展示控件SurfaceView及TextureView對比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期詳解
節(jié)操播放器 https://github.com/lipangit/JieCaoVideoPlayer