MediaPlayer可以用來控制視頻和音頻文件流,也就是說可以通過它播放音樂和視頻。通常如果我們不用第三方的框架,有三種方式可以去播放視頻。
1.VideoView
2.MediaPlayer+SurfaceView
3.MediaPlayer+TextureView
首先VideoView是繼承自SurfaceView,內部維護著一個MediaPlayer,用過VideoView的人都知道,它的控制界面是比較丑陋的,當然我們一般在開發中是不會使用它的。而后面兩種都可以自定義不妨界面,當然它們的區別是一個是SurfaceView,而另一個是TextureView。SurfaceView的原理就是在View的位置上重新創建一個Window,所有界面的繪制和渲染都是在新的Window中執行,不會影響主線程的執行,當然這也存在線程同步問題。另一個問題就是由于SurfaceView的顯示是在新的Windows中,它不會受到View的屬性控制,也不能放在RecycelerView或ListView中,也需要自己管理其生命周期。TextureView是在API14之后被加入的,和SurfaceView不同的是,TextureView不會在View的位置上創建一個新的窗口,所有的界面的繪制渲染都是在View上,這樣就允許TextureView能夠被移動,縮放或做些其它的動畫。TextureView只能在硬件加速窗口中使用,當在軟件中渲染時,TextureView將不會做任何繪制。
好了,說了這么多,決定用第三種封裝自己的播放器,當然本文是參考這篇文章做的
用MediaPlayer+TextureView封裝一個完美實現全屏、小窗口的視頻播放器
TextureView使用
TexureView的使用是比較簡單的,首先你需要做的就是獲取它的SurfaceTexture,它能夠被用來去渲染內容。它的使用大致如下
private TextureView mTextureView;
mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);
//監聽回調
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//SurfaceTexture準備就緒
}
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//SurfaceTexture緩沖大小變化
}
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
//SurfaceTexture即將銷毀
return false;
}
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
//SurfaceTexture狀態更新
}
TextureView不能直接被使用,只有當它attached到一個Window之后,SurfaceTexture準備就緒之后,TextureView才能起作用。當監聽的回調onSurfaceTextureAvailable被調用后,可以通過傳入的的surface關聯Mediaplayer,SurfaceTexture作為數據通道,把從數據源Mediaplayer中獲取到的圖像幀數據轉化為GL外部紋理,交給TextureView作為View heirachy中的一個硬件加速層來顯示,從而實現視頻播放的功能。
//關聯Mediaplayer,之所以要做判斷,是因為當mTextureView重新繪制之后,生命
//周期方法會被回調,監聽器也會被回調,而mediaplayer不會被銷毀任然持
//有SurfaceTexture的引用,所以當生命周期回調之后直接使用持有的
//SurfaceTexture,任然可以繼續播放
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if(mSurfaceTexture == null){
mSurfaceTexture = surface;
mediaPlayerStart();
}else {
mTextureView.setSurfaceTexture(mSurfaceTexture);
}
}
代碼設計
為了使業務邏輯分離解耦,將數據源和控制器部分分成兩個類處理
//數據源播放
public class VideoPlayer extends FrameLayout implements TextureView.SurfaceTextureListener,
VideoPlayerControl{}
//控制器部分
public class VideoPlayerController extends FrameLayout implements
View.OnClickListener,
SeekBar.OnSeekBarChangeListener,
View.OnTouchListener{}
其中數據源部分專門負責播放視頻,處理播放狀態的初始化和狀態變化,而控制器部分專門負責播放界面的 播放,暫停,全屏,小窗口 操作。而兩個類之間通過一個接口VideoPlayerControl關聯。
public interface VideoPlayerControl {
void start(); //播放
void pause(); //暫停
void seekTo(int pos); //進度條拖動
void restart();
void release();
boolean isIdle(); //是否是空閑
boolean isError();
boolean isPreparing();
boolean isPrepared();
boolean isBufferingPlaying();
boolean isBufferingPaused();
boolean isPlaying();
boolean isPaused();
boolean isCompleted();
int getDuration();
int getCurrentProgress();
int getBufferPercent();
FrameLayout getContainer();
boolean isFullScreen(); //全屏
boolean isNormalScreen();//普通窗口
boolean isTinyScreen(); //小窗口
void enterFullScreen(); //進入全屏
boolean exitFullScreen(); //退出全屏
void enterTinyScreen(); //進入小屏
boolean exitTinyScreen(); //退出小屏
}
VideoPlayer
常量
定義幾個常量來標注播放狀態和界面窗口狀態
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; // 播放完成
public static final int PLAYER_NORMAL = 10; // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11; // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12; // 小窗口播放器
mContainer
初始化界面時,mController和mTextureView是添加到mContainer中的,這樣做的好處就是,方便移除和添加窗口,所有的操作只需要通過對mContainer操作來完成
關聯視頻播放器
public void setController(VideoPlayerController controller){
mController = controller;
mController.setVideoPlayer(this);
updateVideoPlayerState();
mContainer.removeView(mController);
LayoutParams lp = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mContainer.addView(mController, lp);
}
我們需要傳入一個controller然后關聯此VideoPlayer就可以了。然后添加到mContainer中就可以了。
設置視頻播放uri
public void setPlayUri(String uri){
mUri = uri;
}
MediaPlayer
要使用MediaPlayer需要設置幾個監聽器,
mMediaPlayer.setOnPreparedListener(mOnPreparedListener); //mediaplayer準備就緒回調
mMediaPlayer.setOnCompletionListener(mOnCompletionListener); //mediaplayer播放完成監聽
mMediaPlayer.setOnErrorListener(mOnErrorListener); //播放錯誤監聽回調
mMediaPlayer.setOnInfoListener(mOnInfoListener); //播放器渲染狀態變化監聽
mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); //播放器緩沖進度0-100回調
初始化和播放
//初始化
private void mediaPlayerInit() {
if(mMediaPlayer == null){
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true); //在播放時屏幕一直開啟著
mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangeListener);
mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
mMediaPlayer.setOnErrorListener(mOnErrorListener);
mMediaPlayer.setOnInfoListener(mOnInfoListener);
mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
}
}
//播放
private void mediaPlayerStart(){
try {
//設置數據源
mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUri));
//設置surface
mMediaPlayer.setSurface(new Surface(mSurfaceTexture));
//異步網絡準備
mMediaPlayer.prepareAsync();
mCurrentState = STATE_PREPARING;
updateVideoPlayerState();
} catch (IOException e) {
e.printStackTrace();
}
}
全屏和小窗口進入和退出
全屏和小窗口差不多,它們的實現原理大致是,先從自己的view中移除mContainer,然后設置LayoutParams,最后在android.R.id.content中添加mContainer就可以了。只要設置不同的lp參數就可以實現全屏和小窗口播放。退出的話就只需要移除然后添加到FrameLayout容器中就可以了。注意全屏要設置屏幕方向,通過setRequestedOrientation請求屏幕方向。ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE為橫屏,SCREEN_ORIENTATION_PORTRAIT為豎屏。
注意: 變化的狀態都可以通過updateVideoPlayerState()回調控制器更新窗口
private void updateVideoPlayerState() {
mController.setControllerState(mCurrentState, mWindowState);
}
VideoPlayerController
這個類為自定義的一個控制器,所有播放器的操作都是通過這個控制器回調播放器VideoPlayer,這個控制器定義了一系列方法控制播放狀態
setTitle 設置頁面標題
setImage 設置封面背景
setTopBottomVisible 頭部和底部是否隱藏
setControllerState 設置播放器工作狀態
updateProgress 更新進度
同時做了一個小窗口界面拖拽
@Override
public boolean onTouch(View v, MotionEvent event) {
if(!mVideoPlayerControl.isTinyScreen()) return super.onTouchEvent(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
startX = event.getRawX();
startY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getRawX();
float endY = event.getRawY();
float dx = endX - startX;
float dy = endY - startY;
LayoutParams params = (LayoutParams) mVideoPlayerControl.getContainer().getLayoutParams();
int left = (int) (params.leftMargin + dx);
int top = (int) (params.topMargin + dy);
int viewHeight = mVideoPlayerControl.getContainer().getHeight() + 50;
int viewWidth = mVideoPlayerControl.getContainer().getWidth();
if(left < -1){
left = 0;
}else if(left > screenWidth - viewWidth){
left = screenWidth - viewWidth;
}
if(top < -1){
top = 0;
}else if(top > screenHeight - viewHeight){
top = screenHeight - viewHeight;
}
params.leftMargin = left;
params.topMargin = top;
mVideoPlayerControl.getContainer().setLayoutParams(params);
startX = endX;
startY = endY;
break;
}
return super.onTouchEvent(event);
}
比較簡單,就不多解釋了。
VideoPlayerManager
這個類的作用就是保證在recyclerview或者listview中播放時只有一個列表可以播放。
/**
* 單例管理視頻播放 在listview或者recyclerview中保證頁面只有一個播放器在播放
*/
public class VideoPlayerManager {
private VideoPlayer mVideoPlayer;
private VideoPlayerManager(){}
private static VideoPlayerManager sInstance;
public static VideoPlayerManager getInstance(){
if(sInstance == null){
sInstance = new VideoPlayerManager();
}
return sInstance;
}
public void releaseMediaplayer(){
if(mVideoPlayer != null){
mVideoPlayer.release();
mVideoPlayer = null;
}
}
public void setCurrentVideoPlayer(VideoPlayer videoPlayer){
mVideoPlayer = videoPlayer;
}
/**
* 釋放資源
*/
public boolean onBackPress(){
if(mVideoPlayer.isFullScreen()){
mVideoPlayer.exitFullScreen();
return true;
}else if(mVideoPlayer.isTinyScreen()){
mVideoPlayer.exitTinyScreen(); //退出小屏
return true;
}
if(mVideoPlayer != null){
mVideoPlayer.release();
}
return false;
}
}
用法
private VideoPlayer videoPlayer;
private VideoPlayerController mController;
videoPlayer = (VideoPlayer) findViewById(R.id.vp);
videoPlayer.setPlayUri("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-33-30.mp4");
mController = new VideoPlayerController(this);
mController.setTitle("辦公室小野開番外了,居然在辦公室開澡堂!老板還點贊?");
mController.setImage("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg");
videoPlayer.setController(mController);
效果
源碼:github