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