项目已添加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