项目已添加IjkPlayer支持,后续逐渐完善其他功能。
地址:https://github.com/xiaoyanger0825/NiceVieoPlayer
为什么使用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<String, String> 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
、清除全屏模式就行了。
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。
不同项目都可能定制不同的控制器(播放操作界面),这里我就不详细分析实现逻辑了,大致功能就类似腾讯视频的热点列表中的播放器。其中横向滑动改变播放进度、左侧上下滑动改变亮度,右侧上下滑动改变亮度等功能在代码中都有实现。代码有点长,就不贴了,需要的直接下载源码。
使用
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,谢谢!。
源码:https://github.com/xiaoyanger0825/NiceVieoPlayer
参考:
Android TextureView简易教程
视频画面帧的展示控件SurfaceView及TextureView对比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期详解
节操播放器 https://github.com/lipangit/JieCaoVideoPlayer